##// END OF EJS Templates
pull-requests: trigger merge simulation during PR creation. Fixes #5396
marcink -
r2168:41032fb6 default
parent child Browse files
Show More
@@ -1,779 +1,780 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 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, repoid, pullrequestid):
46 def get_pull_request(request, apiuser, repoid, pullrequestid):
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: Repository name or repository ID from where the pull
52 :param repoid: Repository name or repository ID from where the pull
53 request was opened.
53 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 get_repo_or_error(repoid)
124 get_repo_or_error(repoid)
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if not PullRequestModel().check_user_read(
126 if not PullRequestModel().check_user_read(
127 pull_request, apiuser, api=True):
127 pull_request, apiuser, api=True):
128 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
128 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
129 data = pull_request.get_api_data()
129 data = pull_request.get_api_data()
130 return data
130 return data
131
131
132
132
133 @jsonrpc_method()
133 @jsonrpc_method()
134 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
134 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
135 """
135 """
136 Get all pull requests from the repository specified in `repoid`.
136 Get all pull requests from the repository specified in `repoid`.
137
137
138 :param apiuser: This is filled automatically from the |authtoken|.
138 :param apiuser: This is filled automatically from the |authtoken|.
139 :type apiuser: AuthUser
139 :type apiuser: AuthUser
140 :param repoid: Repository name or repository ID.
140 :param repoid: Repository name or repository ID.
141 :type repoid: str or int
141 :type repoid: str or int
142 :param status: Only return pull requests with the specified status.
142 :param status: Only return pull requests with the specified status.
143 Valid options are.
143 Valid options are.
144 * ``new`` (default)
144 * ``new`` (default)
145 * ``open``
145 * ``open``
146 * ``closed``
146 * ``closed``
147 :type status: str
147 :type status: str
148
148
149 Example output:
149 Example output:
150
150
151 .. code-block:: bash
151 .. code-block:: bash
152
152
153 "id": <id_given_in_input>,
153 "id": <id_given_in_input>,
154 "result":
154 "result":
155 [
155 [
156 ...
156 ...
157 {
157 {
158 "pull_request_id": "<pull_request_id>",
158 "pull_request_id": "<pull_request_id>",
159 "url": "<url>",
159 "url": "<url>",
160 "title" : "<title>",
160 "title" : "<title>",
161 "description": "<description>",
161 "description": "<description>",
162 "status": "<status>",
162 "status": "<status>",
163 "created_on": "<date_time_created>",
163 "created_on": "<date_time_created>",
164 "updated_on": "<date_time_updated>",
164 "updated_on": "<date_time_updated>",
165 "commit_ids": [
165 "commit_ids": [
166 ...
166 ...
167 "<commit_id>",
167 "<commit_id>",
168 "<commit_id>",
168 "<commit_id>",
169 ...
169 ...
170 ],
170 ],
171 "review_status": "<review_status>",
171 "review_status": "<review_status>",
172 "mergeable": {
172 "mergeable": {
173 "status": "<bool>",
173 "status": "<bool>",
174 "message: "<message>",
174 "message: "<message>",
175 },
175 },
176 "source": {
176 "source": {
177 "clone_url": "<clone_url>",
177 "clone_url": "<clone_url>",
178 "reference":
178 "reference":
179 {
179 {
180 "name": "<name>",
180 "name": "<name>",
181 "type": "<type>",
181 "type": "<type>",
182 "commit_id": "<commit_id>",
182 "commit_id": "<commit_id>",
183 }
183 }
184 },
184 },
185 "target": {
185 "target": {
186 "clone_url": "<clone_url>",
186 "clone_url": "<clone_url>",
187 "reference":
187 "reference":
188 {
188 {
189 "name": "<name>",
189 "name": "<name>",
190 "type": "<type>",
190 "type": "<type>",
191 "commit_id": "<commit_id>",
191 "commit_id": "<commit_id>",
192 }
192 }
193 },
193 },
194 "merge": {
194 "merge": {
195 "clone_url": "<clone_url>",
195 "clone_url": "<clone_url>",
196 "reference":
196 "reference":
197 {
197 {
198 "name": "<name>",
198 "name": "<name>",
199 "type": "<type>",
199 "type": "<type>",
200 "commit_id": "<commit_id>",
200 "commit_id": "<commit_id>",
201 }
201 }
202 },
202 },
203 "author": <user_obj>,
203 "author": <user_obj>,
204 "reviewers": [
204 "reviewers": [
205 ...
205 ...
206 {
206 {
207 "user": "<user_obj>",
207 "user": "<user_obj>",
208 "review_status": "<review_status>",
208 "review_status": "<review_status>",
209 }
209 }
210 ...
210 ...
211 ]
211 ]
212 }
212 }
213 ...
213 ...
214 ],
214 ],
215 "error": null
215 "error": null
216
216
217 """
217 """
218 repo = get_repo_or_error(repoid)
218 repo = get_repo_or_error(repoid)
219 if not has_superadmin_permission(apiuser):
219 if not has_superadmin_permission(apiuser):
220 _perms = (
220 _perms = (
221 'repository.admin', 'repository.write', 'repository.read',)
221 'repository.admin', 'repository.write', 'repository.read',)
222 validate_repo_permissions(apiuser, repoid, repo, _perms)
222 validate_repo_permissions(apiuser, repoid, repo, _perms)
223
223
224 status = Optional.extract(status)
224 status = Optional.extract(status)
225 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
225 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
226 data = [pr.get_api_data() for pr in pull_requests]
226 data = [pr.get_api_data() for pr in pull_requests]
227 return data
227 return data
228
228
229
229
230 @jsonrpc_method()
230 @jsonrpc_method()
231 def merge_pull_request(
231 def merge_pull_request(
232 request, apiuser, repoid, pullrequestid,
232 request, apiuser, repoid, pullrequestid,
233 userid=Optional(OAttr('apiuser'))):
233 userid=Optional(OAttr('apiuser'))):
234 """
234 """
235 Merge the pull request specified by `pullrequestid` into its target
235 Merge the pull request specified by `pullrequestid` into its target
236 repository.
236 repository.
237
237
238 :param apiuser: This is filled automatically from the |authtoken|.
238 :param apiuser: This is filled automatically from the |authtoken|.
239 :type apiuser: AuthUser
239 :type apiuser: AuthUser
240 :param repoid: The Repository name or repository ID of the
240 :param repoid: The Repository name or repository ID of the
241 target repository to which the |pr| is to be merged.
241 target repository to which the |pr| is to be merged.
242 :type repoid: str or int
242 :type repoid: str or int
243 :param pullrequestid: ID of the pull request which shall be merged.
243 :param pullrequestid: ID of the pull request which shall be merged.
244 :type pullrequestid: int
244 :type pullrequestid: int
245 :param userid: Merge the pull request as this user.
245 :param userid: Merge the pull request as this user.
246 :type userid: Optional(str or int)
246 :type userid: Optional(str or int)
247
247
248 Example output:
248 Example output:
249
249
250 .. code-block:: bash
250 .. code-block:: bash
251
251
252 "id": <id_given_in_input>,
252 "id": <id_given_in_input>,
253 "result": {
253 "result": {
254 "executed": "<bool>",
254 "executed": "<bool>",
255 "failure_reason": "<int>",
255 "failure_reason": "<int>",
256 "merge_commit_id": "<merge_commit_id>",
256 "merge_commit_id": "<merge_commit_id>",
257 "possible": "<bool>",
257 "possible": "<bool>",
258 "merge_ref": {
258 "merge_ref": {
259 "commit_id": "<commit_id>",
259 "commit_id": "<commit_id>",
260 "type": "<type>",
260 "type": "<type>",
261 "name": "<name>"
261 "name": "<name>"
262 }
262 }
263 },
263 },
264 "error": null
264 "error": null
265 """
265 """
266 repo = get_repo_or_error(repoid)
266 repo = get_repo_or_error(repoid)
267 if not isinstance(userid, Optional):
267 if not isinstance(userid, Optional):
268 if (has_superadmin_permission(apiuser) or
268 if (has_superadmin_permission(apiuser) or
269 HasRepoPermissionAnyApi('repository.admin')(
269 HasRepoPermissionAnyApi('repository.admin')(
270 user=apiuser, repo_name=repo.repo_name)):
270 user=apiuser, repo_name=repo.repo_name)):
271 apiuser = get_user_or_error(userid)
271 apiuser = get_user_or_error(userid)
272 else:
272 else:
273 raise JSONRPCError('userid is not the same as your user')
273 raise JSONRPCError('userid is not the same as your user')
274
274
275 pull_request = get_pull_request_or_error(pullrequestid)
275 pull_request = get_pull_request_or_error(pullrequestid)
276
276
277 check = MergeCheck.validate(pull_request, user=apiuser)
277 check = MergeCheck.validate(
278 pull_request, user=apiuser, translator=request.translate)
278 merge_possible = not check.failed
279 merge_possible = not check.failed
279
280
280 if not merge_possible:
281 if not merge_possible:
281 error_messages = []
282 error_messages = []
282 for err_type, error_msg in check.errors:
283 for err_type, error_msg in check.errors:
283 error_msg = request.translate(error_msg)
284 error_msg = request.translate(error_msg)
284 error_messages.append(error_msg)
285 error_messages.append(error_msg)
285
286
286 reasons = ','.join(error_messages)
287 reasons = ','.join(error_messages)
287 raise JSONRPCError(
288 raise JSONRPCError(
288 'merge not possible for following reasons: {}'.format(reasons))
289 'merge not possible for following reasons: {}'.format(reasons))
289
290
290 target_repo = pull_request.target_repo
291 target_repo = pull_request.target_repo
291 extras = vcs_operation_context(
292 extras = vcs_operation_context(
292 request.environ, repo_name=target_repo.repo_name,
293 request.environ, repo_name=target_repo.repo_name,
293 username=apiuser.username, action='push',
294 username=apiuser.username, action='push',
294 scm=target_repo.repo_type)
295 scm=target_repo.repo_type)
295 merge_response = PullRequestModel().merge(
296 merge_response = PullRequestModel().merge(
296 pull_request, apiuser, extras=extras)
297 pull_request, apiuser, extras=extras)
297 if merge_response.executed:
298 if merge_response.executed:
298 PullRequestModel().close_pull_request(
299 PullRequestModel().close_pull_request(
299 pull_request.pull_request_id, apiuser)
300 pull_request.pull_request_id, apiuser)
300
301
301 Session().commit()
302 Session().commit()
302
303
303 # In previous versions the merge response directly contained the merge
304 # In previous versions the merge response directly contained the merge
304 # commit id. It is now contained in the merge reference object. To be
305 # commit id. It is now contained in the merge reference object. To be
305 # backwards compatible we have to extract it again.
306 # backwards compatible we have to extract it again.
306 merge_response = merge_response._asdict()
307 merge_response = merge_response._asdict()
307 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
308 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
308
309
309 return merge_response
310 return merge_response
310
311
311
312
312 @jsonrpc_method()
313 @jsonrpc_method()
313 def comment_pull_request(
314 def comment_pull_request(
314 request, apiuser, repoid, pullrequestid, message=Optional(None),
315 request, apiuser, repoid, pullrequestid, message=Optional(None),
315 commit_id=Optional(None), status=Optional(None),
316 commit_id=Optional(None), status=Optional(None),
316 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
317 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
317 resolves_comment_id=Optional(None),
318 resolves_comment_id=Optional(None),
318 userid=Optional(OAttr('apiuser'))):
319 userid=Optional(OAttr('apiuser'))):
319 """
320 """
320 Comment on the pull request specified with the `pullrequestid`,
321 Comment on the pull request specified with the `pullrequestid`,
321 in the |repo| specified by the `repoid`, and optionally change the
322 in the |repo| specified by the `repoid`, and optionally change the
322 review status.
323 review status.
323
324
324 :param apiuser: This is filled automatically from the |authtoken|.
325 :param apiuser: This is filled automatically from the |authtoken|.
325 :type apiuser: AuthUser
326 :type apiuser: AuthUser
326 :param repoid: The repository name or repository ID.
327 :param repoid: The repository name or repository ID.
327 :type repoid: str or int
328 :type repoid: str or int
328 :param pullrequestid: The pull request ID.
329 :param pullrequestid: The pull request ID.
329 :type pullrequestid: int
330 :type pullrequestid: int
330 :param commit_id: Specify the commit_id for which to set a comment. If
331 :param commit_id: Specify the commit_id for which to set a comment. If
331 given commit_id is different than latest in the PR status
332 given commit_id is different than latest in the PR status
332 change won't be performed.
333 change won't be performed.
333 :type commit_id: str
334 :type commit_id: str
334 :param message: The text content of the comment.
335 :param message: The text content of the comment.
335 :type message: str
336 :type message: str
336 :param status: (**Optional**) Set the approval status of the pull
337 :param status: (**Optional**) Set the approval status of the pull
337 request. One of: 'not_reviewed', 'approved', 'rejected',
338 request. One of: 'not_reviewed', 'approved', 'rejected',
338 'under_review'
339 'under_review'
339 :type status: str
340 :type status: str
340 :param comment_type: Comment type, one of: 'note', 'todo'
341 :param comment_type: Comment type, one of: 'note', 'todo'
341 :type comment_type: Optional(str), default: 'note'
342 :type comment_type: Optional(str), default: 'note'
342 :param userid: Comment on the pull request as this user
343 :param userid: Comment on the pull request as this user
343 :type userid: Optional(str or int)
344 :type userid: Optional(str or int)
344
345
345 Example output:
346 Example output:
346
347
347 .. code-block:: bash
348 .. code-block:: bash
348
349
349 id : <id_given_in_input>
350 id : <id_given_in_input>
350 result : {
351 result : {
351 "pull_request_id": "<Integer>",
352 "pull_request_id": "<Integer>",
352 "comment_id": "<Integer>",
353 "comment_id": "<Integer>",
353 "status": {"given": <given_status>,
354 "status": {"given": <given_status>,
354 "was_changed": <bool status_was_actually_changed> },
355 "was_changed": <bool status_was_actually_changed> },
355 },
356 },
356 error : null
357 error : null
357 """
358 """
358 repo = get_repo_or_error(repoid)
359 repo = get_repo_or_error(repoid)
359 if not isinstance(userid, Optional):
360 if not isinstance(userid, Optional):
360 if (has_superadmin_permission(apiuser) or
361 if (has_superadmin_permission(apiuser) or
361 HasRepoPermissionAnyApi('repository.admin')(
362 HasRepoPermissionAnyApi('repository.admin')(
362 user=apiuser, repo_name=repo.repo_name)):
363 user=apiuser, repo_name=repo.repo_name)):
363 apiuser = get_user_or_error(userid)
364 apiuser = get_user_or_error(userid)
364 else:
365 else:
365 raise JSONRPCError('userid is not the same as your user')
366 raise JSONRPCError('userid is not the same as your user')
366
367
367 pull_request = get_pull_request_or_error(pullrequestid)
368 pull_request = get_pull_request_or_error(pullrequestid)
368 if not PullRequestModel().check_user_read(
369 if not PullRequestModel().check_user_read(
369 pull_request, apiuser, api=True):
370 pull_request, apiuser, api=True):
370 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
371 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
371 message = Optional.extract(message)
372 message = Optional.extract(message)
372 status = Optional.extract(status)
373 status = Optional.extract(status)
373 commit_id = Optional.extract(commit_id)
374 commit_id = Optional.extract(commit_id)
374 comment_type = Optional.extract(comment_type)
375 comment_type = Optional.extract(comment_type)
375 resolves_comment_id = Optional.extract(resolves_comment_id)
376 resolves_comment_id = Optional.extract(resolves_comment_id)
376
377
377 if not message and not status:
378 if not message and not status:
378 raise JSONRPCError(
379 raise JSONRPCError(
379 'Both message and status parameters are missing. '
380 'Both message and status parameters are missing. '
380 'At least one is required.')
381 'At least one is required.')
381
382
382 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
383 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
383 status is not None):
384 status is not None):
384 raise JSONRPCError('Unknown comment status: `%s`' % status)
385 raise JSONRPCError('Unknown comment status: `%s`' % status)
385
386
386 if commit_id and commit_id not in pull_request.revisions:
387 if commit_id and commit_id not in pull_request.revisions:
387 raise JSONRPCError(
388 raise JSONRPCError(
388 'Invalid commit_id `%s` for this pull request.' % commit_id)
389 'Invalid commit_id `%s` for this pull request.' % commit_id)
389
390
390 allowed_to_change_status = PullRequestModel().check_user_change_status(
391 allowed_to_change_status = PullRequestModel().check_user_change_status(
391 pull_request, apiuser)
392 pull_request, apiuser)
392
393
393 # if commit_id is passed re-validated if user is allowed to change status
394 # if commit_id is passed re-validated if user is allowed to change status
394 # based on latest commit_id from the PR
395 # based on latest commit_id from the PR
395 if commit_id:
396 if commit_id:
396 commit_idx = pull_request.revisions.index(commit_id)
397 commit_idx = pull_request.revisions.index(commit_id)
397 if commit_idx != 0:
398 if commit_idx != 0:
398 allowed_to_change_status = False
399 allowed_to_change_status = False
399
400
400 if resolves_comment_id:
401 if resolves_comment_id:
401 comment = ChangesetComment.get(resolves_comment_id)
402 comment = ChangesetComment.get(resolves_comment_id)
402 if not comment:
403 if not comment:
403 raise JSONRPCError(
404 raise JSONRPCError(
404 'Invalid resolves_comment_id `%s` for this pull request.'
405 'Invalid resolves_comment_id `%s` for this pull request.'
405 % resolves_comment_id)
406 % resolves_comment_id)
406 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
407 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
407 raise JSONRPCError(
408 raise JSONRPCError(
408 'Comment `%s` is wrong type for setting status to resolved.'
409 'Comment `%s` is wrong type for setting status to resolved.'
409 % resolves_comment_id)
410 % resolves_comment_id)
410
411
411 text = message
412 text = message
412 status_label = ChangesetStatus.get_status_lbl(status)
413 status_label = ChangesetStatus.get_status_lbl(status)
413 if status and allowed_to_change_status:
414 if status and allowed_to_change_status:
414 st_message = ('Status change %(transition_icon)s %(status)s'
415 st_message = ('Status change %(transition_icon)s %(status)s'
415 % {'transition_icon': '>', 'status': status_label})
416 % {'transition_icon': '>', 'status': status_label})
416 text = message or st_message
417 text = message or st_message
417
418
418 rc_config = SettingsModel().get_all_settings()
419 rc_config = SettingsModel().get_all_settings()
419 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
420 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
420
421
421 status_change = status and allowed_to_change_status
422 status_change = status and allowed_to_change_status
422 comment = CommentsModel().create(
423 comment = CommentsModel().create(
423 text=text,
424 text=text,
424 repo=pull_request.target_repo.repo_id,
425 repo=pull_request.target_repo.repo_id,
425 user=apiuser.user_id,
426 user=apiuser.user_id,
426 pull_request=pull_request.pull_request_id,
427 pull_request=pull_request.pull_request_id,
427 f_path=None,
428 f_path=None,
428 line_no=None,
429 line_no=None,
429 status_change=(status_label if status_change else None),
430 status_change=(status_label if status_change else None),
430 status_change_type=(status if status_change else None),
431 status_change_type=(status if status_change else None),
431 closing_pr=False,
432 closing_pr=False,
432 renderer=renderer,
433 renderer=renderer,
433 comment_type=comment_type,
434 comment_type=comment_type,
434 resolves_comment_id=resolves_comment_id
435 resolves_comment_id=resolves_comment_id
435 )
436 )
436
437
437 if allowed_to_change_status and status:
438 if allowed_to_change_status and status:
438 ChangesetStatusModel().set_status(
439 ChangesetStatusModel().set_status(
439 pull_request.target_repo.repo_id,
440 pull_request.target_repo.repo_id,
440 status,
441 status,
441 apiuser.user_id,
442 apiuser.user_id,
442 comment,
443 comment,
443 pull_request=pull_request.pull_request_id
444 pull_request=pull_request.pull_request_id
444 )
445 )
445 Session().flush()
446 Session().flush()
446
447
447 Session().commit()
448 Session().commit()
448 data = {
449 data = {
449 'pull_request_id': pull_request.pull_request_id,
450 'pull_request_id': pull_request.pull_request_id,
450 'comment_id': comment.comment_id if comment else None,
451 'comment_id': comment.comment_id if comment else None,
451 'status': {'given': status, 'was_changed': status_change},
452 'status': {'given': status, 'was_changed': status_change},
452 }
453 }
453 return data
454 return data
454
455
455
456
456 @jsonrpc_method()
457 @jsonrpc_method()
457 def create_pull_request(
458 def create_pull_request(
458 request, apiuser, source_repo, target_repo, source_ref, target_ref,
459 request, apiuser, source_repo, target_repo, source_ref, target_ref,
459 title, description=Optional(''), reviewers=Optional(None)):
460 title, description=Optional(''), reviewers=Optional(None)):
460 """
461 """
461 Creates a new pull request.
462 Creates a new pull request.
462
463
463 Accepts refs in the following formats:
464 Accepts refs in the following formats:
464
465
465 * branch:<branch_name>:<sha>
466 * branch:<branch_name>:<sha>
466 * branch:<branch_name>
467 * branch:<branch_name>
467 * bookmark:<bookmark_name>:<sha> (Mercurial only)
468 * bookmark:<bookmark_name>:<sha> (Mercurial only)
468 * bookmark:<bookmark_name> (Mercurial only)
469 * bookmark:<bookmark_name> (Mercurial only)
469
470
470 :param apiuser: This is filled automatically from the |authtoken|.
471 :param apiuser: This is filled automatically from the |authtoken|.
471 :type apiuser: AuthUser
472 :type apiuser: AuthUser
472 :param source_repo: Set the source repository name.
473 :param source_repo: Set the source repository name.
473 :type source_repo: str
474 :type source_repo: str
474 :param target_repo: Set the target repository name.
475 :param target_repo: Set the target repository name.
475 :type target_repo: str
476 :type target_repo: str
476 :param source_ref: Set the source ref name.
477 :param source_ref: Set the source ref name.
477 :type source_ref: str
478 :type source_ref: str
478 :param target_ref: Set the target ref name.
479 :param target_ref: Set the target ref name.
479 :type target_ref: str
480 :type target_ref: str
480 :param title: Set the pull request title.
481 :param title: Set the pull request title.
481 :type title: str
482 :type title: str
482 :param description: Set the pull request description.
483 :param description: Set the pull request description.
483 :type description: Optional(str)
484 :type description: Optional(str)
484 :param reviewers: Set the new pull request reviewers list.
485 :param reviewers: Set the new pull request reviewers list.
485 Reviewer defined by review rules will be added automatically to the
486 Reviewer defined by review rules will be added automatically to the
486 defined list.
487 defined list.
487 :type reviewers: Optional(list)
488 :type reviewers: Optional(list)
488 Accepts username strings or objects of the format:
489 Accepts username strings or objects of the format:
489
490
490 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
491 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
491 """
492 """
492
493
493 source_db_repo = get_repo_or_error(source_repo)
494 source_db_repo = get_repo_or_error(source_repo)
494 target_db_repo = get_repo_or_error(target_repo)
495 target_db_repo = get_repo_or_error(target_repo)
495 if not has_superadmin_permission(apiuser):
496 if not has_superadmin_permission(apiuser):
496 _perms = ('repository.admin', 'repository.write', 'repository.read',)
497 _perms = ('repository.admin', 'repository.write', 'repository.read',)
497 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
498 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
498
499
499 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
500 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
500 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
501 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
501 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
502 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
502 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
503 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
503 source_scm = source_db_repo.scm_instance()
504 source_scm = source_db_repo.scm_instance()
504 target_scm = target_db_repo.scm_instance()
505 target_scm = target_db_repo.scm_instance()
505
506
506 commit_ranges = target_scm.compare(
507 commit_ranges = target_scm.compare(
507 target_commit.raw_id, source_commit.raw_id, source_scm,
508 target_commit.raw_id, source_commit.raw_id, source_scm,
508 merge=True, pre_load=[])
509 merge=True, pre_load=[])
509
510
510 ancestor = target_scm.get_common_ancestor(
511 ancestor = target_scm.get_common_ancestor(
511 target_commit.raw_id, source_commit.raw_id, source_scm)
512 target_commit.raw_id, source_commit.raw_id, source_scm)
512
513
513 if not commit_ranges:
514 if not commit_ranges:
514 raise JSONRPCError('no commits found')
515 raise JSONRPCError('no commits found')
515
516
516 if not ancestor:
517 if not ancestor:
517 raise JSONRPCError('no common ancestor found')
518 raise JSONRPCError('no common ancestor found')
518
519
519 reviewer_objects = Optional.extract(reviewers) or []
520 reviewer_objects = Optional.extract(reviewers) or []
520
521
521 if reviewer_objects:
522 if reviewer_objects:
522 schema = ReviewerListSchema()
523 schema = ReviewerListSchema()
523 try:
524 try:
524 reviewer_objects = schema.deserialize(reviewer_objects)
525 reviewer_objects = schema.deserialize(reviewer_objects)
525 except Invalid as err:
526 except Invalid as err:
526 raise JSONRPCValidationError(colander_exc=err)
527 raise JSONRPCValidationError(colander_exc=err)
527
528
528 # validate users
529 # validate users
529 for reviewer_object in reviewer_objects:
530 for reviewer_object in reviewer_objects:
530 user = get_user_or_error(reviewer_object['username'])
531 user = get_user_or_error(reviewer_object['username'])
531 reviewer_object['user_id'] = user.user_id
532 reviewer_object['user_id'] = user.user_id
532
533
533 get_default_reviewers_data, get_validated_reviewers = \
534 get_default_reviewers_data, get_validated_reviewers = \
534 PullRequestModel().get_reviewer_functions()
535 PullRequestModel().get_reviewer_functions()
535
536
536 reviewer_rules = get_default_reviewers_data(
537 reviewer_rules = get_default_reviewers_data(
537 apiuser.get_instance(), source_db_repo,
538 apiuser.get_instance(), source_db_repo,
538 source_commit, target_db_repo, target_commit)
539 source_commit, target_db_repo, target_commit)
539
540
540 # specified rules are later re-validated, thus we can assume users will
541 # specified rules are later re-validated, thus we can assume users will
541 # eventually provide those that meet the reviewer criteria.
542 # eventually provide those that meet the reviewer criteria.
542 if not reviewer_objects:
543 if not reviewer_objects:
543 reviewer_objects = reviewer_rules['reviewers']
544 reviewer_objects = reviewer_rules['reviewers']
544
545
545 try:
546 try:
546 reviewers = get_validated_reviewers(
547 reviewers = get_validated_reviewers(
547 reviewer_objects, reviewer_rules)
548 reviewer_objects, reviewer_rules)
548 except ValueError as e:
549 except ValueError as e:
549 raise JSONRPCError('Reviewers Validation: {}'.format(e))
550 raise JSONRPCError('Reviewers Validation: {}'.format(e))
550
551
551 pull_request_model = PullRequestModel()
552 pull_request_model = PullRequestModel()
552 pull_request = pull_request_model.create(
553 pull_request = pull_request_model.create(
553 created_by=apiuser.user_id,
554 created_by=apiuser.user_id,
554 source_repo=source_repo,
555 source_repo=source_repo,
555 source_ref=full_source_ref,
556 source_ref=full_source_ref,
556 target_repo=target_repo,
557 target_repo=target_repo,
557 target_ref=full_target_ref,
558 target_ref=full_target_ref,
558 revisions=reversed(
559 revisions=reversed(
559 [commit.raw_id for commit in reversed(commit_ranges)]),
560 [commit.raw_id for commit in reversed(commit_ranges)]),
560 reviewers=reviewers,
561 reviewers=reviewers,
561 title=title,
562 title=title,
562 description=Optional.extract(description)
563 description=Optional.extract(description)
563 )
564 )
564
565
565 Session().commit()
566 Session().commit()
566 data = {
567 data = {
567 'msg': 'Created new pull request `{}`'.format(title),
568 'msg': 'Created new pull request `{}`'.format(title),
568 'pull_request_id': pull_request.pull_request_id,
569 'pull_request_id': pull_request.pull_request_id,
569 }
570 }
570 return data
571 return data
571
572
572
573
573 @jsonrpc_method()
574 @jsonrpc_method()
574 def update_pull_request(
575 def update_pull_request(
575 request, apiuser, repoid, pullrequestid, title=Optional(''),
576 request, apiuser, repoid, pullrequestid, title=Optional(''),
576 description=Optional(''), reviewers=Optional(None),
577 description=Optional(''), reviewers=Optional(None),
577 update_commits=Optional(None)):
578 update_commits=Optional(None)):
578 """
579 """
579 Updates a pull request.
580 Updates a pull request.
580
581
581 :param apiuser: This is filled automatically from the |authtoken|.
582 :param apiuser: This is filled automatically from the |authtoken|.
582 :type apiuser: AuthUser
583 :type apiuser: AuthUser
583 :param repoid: The repository name or repository ID.
584 :param repoid: The repository name or repository ID.
584 :type repoid: str or int
585 :type repoid: str or int
585 :param pullrequestid: The pull request ID.
586 :param pullrequestid: The pull request ID.
586 :type pullrequestid: int
587 :type pullrequestid: int
587 :param title: Set the pull request title.
588 :param title: Set the pull request title.
588 :type title: str
589 :type title: str
589 :param description: Update pull request description.
590 :param description: Update pull request description.
590 :type description: Optional(str)
591 :type description: Optional(str)
591 :param reviewers: Update pull request reviewers list with new value.
592 :param reviewers: Update pull request reviewers list with new value.
592 :type reviewers: Optional(list)
593 :type reviewers: Optional(list)
593 Accepts username strings or objects of the format:
594 Accepts username strings or objects of the format:
594
595
595 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
596 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
596
597
597 :param update_commits: Trigger update of commits for this pull request
598 :param update_commits: Trigger update of commits for this pull request
598 :type: update_commits: Optional(bool)
599 :type: update_commits: Optional(bool)
599
600
600 Example output:
601 Example output:
601
602
602 .. code-block:: bash
603 .. code-block:: bash
603
604
604 id : <id_given_in_input>
605 id : <id_given_in_input>
605 result : {
606 result : {
606 "msg": "Updated pull request `63`",
607 "msg": "Updated pull request `63`",
607 "pull_request": <pull_request_object>,
608 "pull_request": <pull_request_object>,
608 "updated_reviewers": {
609 "updated_reviewers": {
609 "added": [
610 "added": [
610 "username"
611 "username"
611 ],
612 ],
612 "removed": []
613 "removed": []
613 },
614 },
614 "updated_commits": {
615 "updated_commits": {
615 "added": [
616 "added": [
616 "<sha1_hash>"
617 "<sha1_hash>"
617 ],
618 ],
618 "common": [
619 "common": [
619 "<sha1_hash>",
620 "<sha1_hash>",
620 "<sha1_hash>",
621 "<sha1_hash>",
621 ],
622 ],
622 "removed": []
623 "removed": []
623 }
624 }
624 }
625 }
625 error : null
626 error : null
626 """
627 """
627
628
628 repo = get_repo_or_error(repoid)
629 repo = get_repo_or_error(repoid)
629 pull_request = get_pull_request_or_error(pullrequestid)
630 pull_request = get_pull_request_or_error(pullrequestid)
630 if not PullRequestModel().check_user_update(
631 if not PullRequestModel().check_user_update(
631 pull_request, apiuser, api=True):
632 pull_request, apiuser, api=True):
632 raise JSONRPCError(
633 raise JSONRPCError(
633 'pull request `%s` update failed, no permission to update.' % (
634 'pull request `%s` update failed, no permission to update.' % (
634 pullrequestid,))
635 pullrequestid,))
635 if pull_request.is_closed():
636 if pull_request.is_closed():
636 raise JSONRPCError(
637 raise JSONRPCError(
637 'pull request `%s` update failed, pull request is closed' % (
638 'pull request `%s` update failed, pull request is closed' % (
638 pullrequestid,))
639 pullrequestid,))
639
640
640 reviewer_objects = Optional.extract(reviewers) or []
641 reviewer_objects = Optional.extract(reviewers) or []
641
642
642 if reviewer_objects:
643 if reviewer_objects:
643 schema = ReviewerListSchema()
644 schema = ReviewerListSchema()
644 try:
645 try:
645 reviewer_objects = schema.deserialize(reviewer_objects)
646 reviewer_objects = schema.deserialize(reviewer_objects)
646 except Invalid as err:
647 except Invalid as err:
647 raise JSONRPCValidationError(colander_exc=err)
648 raise JSONRPCValidationError(colander_exc=err)
648
649
649 # validate users
650 # validate users
650 for reviewer_object in reviewer_objects:
651 for reviewer_object in reviewer_objects:
651 user = get_user_or_error(reviewer_object['username'])
652 user = get_user_or_error(reviewer_object['username'])
652 reviewer_object['user_id'] = user.user_id
653 reviewer_object['user_id'] = user.user_id
653
654
654 get_default_reviewers_data, get_validated_reviewers = \
655 get_default_reviewers_data, get_validated_reviewers = \
655 PullRequestModel().get_reviewer_functions()
656 PullRequestModel().get_reviewer_functions()
656
657
657 # re-use stored rules
658 # re-use stored rules
658 reviewer_rules = pull_request.reviewer_data
659 reviewer_rules = pull_request.reviewer_data
659 try:
660 try:
660 reviewers = get_validated_reviewers(
661 reviewers = get_validated_reviewers(
661 reviewer_objects, reviewer_rules)
662 reviewer_objects, reviewer_rules)
662 except ValueError as e:
663 except ValueError as e:
663 raise JSONRPCError('Reviewers Validation: {}'.format(e))
664 raise JSONRPCError('Reviewers Validation: {}'.format(e))
664 else:
665 else:
665 reviewers = []
666 reviewers = []
666
667
667 title = Optional.extract(title)
668 title = Optional.extract(title)
668 description = Optional.extract(description)
669 description = Optional.extract(description)
669 if title or description:
670 if title or description:
670 PullRequestModel().edit(
671 PullRequestModel().edit(
671 pull_request, title or pull_request.title,
672 pull_request, title or pull_request.title,
672 description or pull_request.description, apiuser)
673 description or pull_request.description, apiuser)
673 Session().commit()
674 Session().commit()
674
675
675 commit_changes = {"added": [], "common": [], "removed": []}
676 commit_changes = {"added": [], "common": [], "removed": []}
676 if str2bool(Optional.extract(update_commits)):
677 if str2bool(Optional.extract(update_commits)):
677 if PullRequestModel().has_valid_update_type(pull_request):
678 if PullRequestModel().has_valid_update_type(pull_request):
678 update_response = PullRequestModel().update_commits(
679 update_response = PullRequestModel().update_commits(
679 pull_request)
680 pull_request)
680 commit_changes = update_response.changes or commit_changes
681 commit_changes = update_response.changes or commit_changes
681 Session().commit()
682 Session().commit()
682
683
683 reviewers_changes = {"added": [], "removed": []}
684 reviewers_changes = {"added": [], "removed": []}
684 if reviewers:
685 if reviewers:
685 added_reviewers, removed_reviewers = \
686 added_reviewers, removed_reviewers = \
686 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
687 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
687
688
688 reviewers_changes['added'] = sorted(
689 reviewers_changes['added'] = sorted(
689 [get_user_or_error(n).username for n in added_reviewers])
690 [get_user_or_error(n).username for n in added_reviewers])
690 reviewers_changes['removed'] = sorted(
691 reviewers_changes['removed'] = sorted(
691 [get_user_or_error(n).username for n in removed_reviewers])
692 [get_user_or_error(n).username for n in removed_reviewers])
692 Session().commit()
693 Session().commit()
693
694
694 data = {
695 data = {
695 'msg': 'Updated pull request `{}`'.format(
696 'msg': 'Updated pull request `{}`'.format(
696 pull_request.pull_request_id),
697 pull_request.pull_request_id),
697 'pull_request': pull_request.get_api_data(),
698 'pull_request': pull_request.get_api_data(),
698 'updated_commits': commit_changes,
699 'updated_commits': commit_changes,
699 'updated_reviewers': reviewers_changes
700 'updated_reviewers': reviewers_changes
700 }
701 }
701
702
702 return data
703 return data
703
704
704
705
705 @jsonrpc_method()
706 @jsonrpc_method()
706 def close_pull_request(
707 def close_pull_request(
707 request, apiuser, repoid, pullrequestid,
708 request, apiuser, repoid, pullrequestid,
708 userid=Optional(OAttr('apiuser')), message=Optional('')):
709 userid=Optional(OAttr('apiuser')), message=Optional('')):
709 """
710 """
710 Close the pull request specified by `pullrequestid`.
711 Close the pull request specified by `pullrequestid`.
711
712
712 :param apiuser: This is filled automatically from the |authtoken|.
713 :param apiuser: This is filled automatically from the |authtoken|.
713 :type apiuser: AuthUser
714 :type apiuser: AuthUser
714 :param repoid: Repository name or repository ID to which the pull
715 :param repoid: Repository name or repository ID to which the pull
715 request belongs.
716 request belongs.
716 :type repoid: str or int
717 :type repoid: str or int
717 :param pullrequestid: ID of the pull request to be closed.
718 :param pullrequestid: ID of the pull request to be closed.
718 :type pullrequestid: int
719 :type pullrequestid: int
719 :param userid: Close the pull request as this user.
720 :param userid: Close the pull request as this user.
720 :type userid: Optional(str or int)
721 :type userid: Optional(str or int)
721 :param message: Optional message to close the Pull Request with. If not
722 :param message: Optional message to close the Pull Request with. If not
722 specified it will be generated automatically.
723 specified it will be generated automatically.
723 :type message: Optional(str)
724 :type message: Optional(str)
724
725
725 Example output:
726 Example output:
726
727
727 .. code-block:: bash
728 .. code-block:: bash
728
729
729 "id": <id_given_in_input>,
730 "id": <id_given_in_input>,
730 "result": {
731 "result": {
731 "pull_request_id": "<int>",
732 "pull_request_id": "<int>",
732 "close_status": "<str:status_lbl>,
733 "close_status": "<str:status_lbl>,
733 "closed": "<bool>"
734 "closed": "<bool>"
734 },
735 },
735 "error": null
736 "error": null
736
737
737 """
738 """
738 _ = request.translate
739 _ = request.translate
739
740
740 repo = get_repo_or_error(repoid)
741 repo = get_repo_or_error(repoid)
741 if not isinstance(userid, Optional):
742 if not isinstance(userid, Optional):
742 if (has_superadmin_permission(apiuser) or
743 if (has_superadmin_permission(apiuser) or
743 HasRepoPermissionAnyApi('repository.admin')(
744 HasRepoPermissionAnyApi('repository.admin')(
744 user=apiuser, repo_name=repo.repo_name)):
745 user=apiuser, repo_name=repo.repo_name)):
745 apiuser = get_user_or_error(userid)
746 apiuser = get_user_or_error(userid)
746 else:
747 else:
747 raise JSONRPCError('userid is not the same as your user')
748 raise JSONRPCError('userid is not the same as your user')
748
749
749 pull_request = get_pull_request_or_error(pullrequestid)
750 pull_request = get_pull_request_or_error(pullrequestid)
750
751
751 if pull_request.is_closed():
752 if pull_request.is_closed():
752 raise JSONRPCError(
753 raise JSONRPCError(
753 'pull request `%s` is already closed' % (pullrequestid,))
754 'pull request `%s` is already closed' % (pullrequestid,))
754
755
755 # only owner or admin or person with write permissions
756 # only owner or admin or person with write permissions
756 allowed_to_close = PullRequestModel().check_user_update(
757 allowed_to_close = PullRequestModel().check_user_update(
757 pull_request, apiuser, api=True)
758 pull_request, apiuser, api=True)
758
759
759 if not allowed_to_close:
760 if not allowed_to_close:
760 raise JSONRPCError(
761 raise JSONRPCError(
761 'pull request `%s` close failed, no permission to close.' % (
762 'pull request `%s` close failed, no permission to close.' % (
762 pullrequestid,))
763 pullrequestid,))
763
764
764 # message we're using to close the PR, else it's automatically generated
765 # message we're using to close the PR, else it's automatically generated
765 message = Optional.extract(message)
766 message = Optional.extract(message)
766
767
767 # finally close the PR, with proper message comment
768 # finally close the PR, with proper message comment
768 comment, status = PullRequestModel().close_pull_request_with_comment(
769 comment, status = PullRequestModel().close_pull_request_with_comment(
769 pull_request, apiuser, repo, message=message)
770 pull_request, apiuser, repo, message=message)
770 status_lbl = ChangesetStatus.get_status_lbl(status)
771 status_lbl = ChangesetStatus.get_status_lbl(status)
771
772
772 Session().commit()
773 Session().commit()
773
774
774 data = {
775 data = {
775 'pull_request_id': pull_request.pull_request_id,
776 'pull_request_id': pull_request.pull_request_id,
776 'close_status': status_lbl,
777 'close_status': status_lbl,
777 'closed': True,
778 'closed': True,
778 }
779 }
779 return data
780 return data
@@ -1,1135 +1,1135 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.tests import (
33 from rhodecode.tests import (
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
56 }[name].format(**kwargs)
56 }[name].format(**kwargs)
57
57
58 if params:
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 return base_url
60 return base_url
61
61
62
62
63 @pytest.mark.usefixtures('app', 'autologin_user')
63 @pytest.mark.usefixtures('app', 'autologin_user')
64 @pytest.mark.backends("git", "hg")
64 @pytest.mark.backends("git", "hg")
65 class TestPullrequestsView(object):
65 class TestPullrequestsView(object):
66
66
67 def test_index(self, backend):
67 def test_index(self, backend):
68 self.app.get(route_path(
68 self.app.get(route_path(
69 'pullrequest_new',
69 'pullrequest_new',
70 repo_name=backend.repo_name))
70 repo_name=backend.repo_name))
71
71
72 def test_option_menu_create_pull_request_exists(self, backend):
72 def test_option_menu_create_pull_request_exists(self, backend):
73 repo_name = backend.repo_name
73 repo_name = backend.repo_name
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75
75
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 'pullrequest_new', repo_name=repo_name)
77 'pullrequest_new', repo_name=repo_name)
78 response.mustcontain(create_pr_link)
78 response.mustcontain(create_pr_link)
79
79
80 def test_create_pr_form_with_raw_commit_id(self, backend):
80 def test_create_pr_form_with_raw_commit_id(self, backend):
81 repo = backend.repo
81 repo = backend.repo
82
82
83 self.app.get(
83 self.app.get(
84 route_path('pullrequest_new',
84 route_path('pullrequest_new',
85 repo_name=repo.repo_name,
85 repo_name=repo.repo_name,
86 commit=repo.get_commit().raw_id),
86 commit=repo.get_commit().raw_id),
87 status=200)
87 status=200)
88
88
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 def test_show(self, pr_util, pr_merge_enabled):
90 def test_show(self, pr_util, pr_merge_enabled):
91 pull_request = pr_util.create_pull_request(
91 pull_request = pr_util.create_pull_request(
92 mergeable=pr_merge_enabled, enable_notifications=False)
92 mergeable=pr_merge_enabled, enable_notifications=False)
93
93
94 response = self.app.get(route_path(
94 response = self.app.get(route_path(
95 'pullrequest_show',
95 'pullrequest_show',
96 repo_name=pull_request.target_repo.scm_instance().name,
96 repo_name=pull_request.target_repo.scm_instance().name,
97 pull_request_id=pull_request.pull_request_id))
97 pull_request_id=pull_request.pull_request_id))
98
98
99 for commit_id in pull_request.revisions:
99 for commit_id in pull_request.revisions:
100 response.mustcontain(commit_id)
100 response.mustcontain(commit_id)
101
101
102 assert pull_request.target_ref_parts.type in response
102 assert pull_request.target_ref_parts.type in response
103 assert pull_request.target_ref_parts.name in response
103 assert pull_request.target_ref_parts.name in response
104 target_clone_url = pull_request.target_repo.clone_url()
104 target_clone_url = pull_request.target_repo.clone_url()
105 assert target_clone_url in response
105 assert target_clone_url in response
106
106
107 assert 'class="pull-request-merge"' in response
107 assert 'class="pull-request-merge"' in response
108 assert (
108 assert (
109 'Server-side pull request merging is disabled.'
109 'Server-side pull request merging is disabled.'
110 in response) != pr_merge_enabled
110 in response) != pr_merge_enabled
111
111
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
113 # Logout
113 # Logout
114 response = self.app.post(
114 response = self.app.post(
115 h.route_path('logout'),
115 h.route_path('logout'),
116 params={'csrf_token': csrf_token})
116 params={'csrf_token': csrf_token})
117 # Login as regular user
117 # Login as regular user
118 response = self.app.post(h.route_path('login'),
118 response = self.app.post(h.route_path('login'),
119 {'username': TEST_USER_REGULAR_LOGIN,
119 {'username': TEST_USER_REGULAR_LOGIN,
120 'password': 'test12'})
120 'password': 'test12'})
121
121
122 pull_request = pr_util.create_pull_request(
122 pull_request = pr_util.create_pull_request(
123 author=TEST_USER_REGULAR_LOGIN)
123 author=TEST_USER_REGULAR_LOGIN)
124
124
125 response = self.app.get(route_path(
125 response = self.app.get(route_path(
126 'pullrequest_show',
126 'pullrequest_show',
127 repo_name=pull_request.target_repo.scm_instance().name,
127 repo_name=pull_request.target_repo.scm_instance().name,
128 pull_request_id=pull_request.pull_request_id))
128 pull_request_id=pull_request.pull_request_id))
129
129
130 response.mustcontain('Server-side pull request merging is disabled.')
130 response.mustcontain('Server-side pull request merging is disabled.')
131
131
132 assert_response = response.assert_response()
132 assert_response = response.assert_response()
133 # for regular user without a merge permissions, we don't see it
133 # for regular user without a merge permissions, we don't see it
134 assert_response.no_element_exists('#close-pull-request-action')
134 assert_response.no_element_exists('#close-pull-request-action')
135
135
136 user_util.grant_user_permission_to_repo(
136 user_util.grant_user_permission_to_repo(
137 pull_request.target_repo,
137 pull_request.target_repo,
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
139 'repository.write')
139 'repository.write')
140 response = self.app.get(route_path(
140 response = self.app.get(route_path(
141 'pullrequest_show',
141 'pullrequest_show',
142 repo_name=pull_request.target_repo.scm_instance().name,
142 repo_name=pull_request.target_repo.scm_instance().name,
143 pull_request_id=pull_request.pull_request_id))
143 pull_request_id=pull_request.pull_request_id))
144
144
145 response.mustcontain('Server-side pull request merging is disabled.')
145 response.mustcontain('Server-side pull request merging is disabled.')
146
146
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 # now regular user has a merge permissions, we have CLOSE button
148 # now regular user has a merge permissions, we have CLOSE button
149 assert_response.one_element_exists('#close-pull-request-action')
149 assert_response.one_element_exists('#close-pull-request-action')
150
150
151 def test_show_invalid_commit_id(self, pr_util):
151 def test_show_invalid_commit_id(self, pr_util):
152 # Simulating invalid revisions which will cause a lookup error
152 # Simulating invalid revisions which will cause a lookup error
153 pull_request = pr_util.create_pull_request()
153 pull_request = pr_util.create_pull_request()
154 pull_request.revisions = ['invalid']
154 pull_request.revisions = ['invalid']
155 Session().add(pull_request)
155 Session().add(pull_request)
156 Session().commit()
156 Session().commit()
157
157
158 response = self.app.get(route_path(
158 response = self.app.get(route_path(
159 'pullrequest_show',
159 'pullrequest_show',
160 repo_name=pull_request.target_repo.scm_instance().name,
160 repo_name=pull_request.target_repo.scm_instance().name,
161 pull_request_id=pull_request.pull_request_id))
161 pull_request_id=pull_request.pull_request_id))
162
162
163 for commit_id in pull_request.revisions:
163 for commit_id in pull_request.revisions:
164 response.mustcontain(commit_id)
164 response.mustcontain(commit_id)
165
165
166 def test_show_invalid_source_reference(self, pr_util):
166 def test_show_invalid_source_reference(self, pr_util):
167 pull_request = pr_util.create_pull_request()
167 pull_request = pr_util.create_pull_request()
168 pull_request.source_ref = 'branch:b:invalid'
168 pull_request.source_ref = 'branch:b:invalid'
169 Session().add(pull_request)
169 Session().add(pull_request)
170 Session().commit()
170 Session().commit()
171
171
172 self.app.get(route_path(
172 self.app.get(route_path(
173 'pullrequest_show',
173 'pullrequest_show',
174 repo_name=pull_request.target_repo.scm_instance().name,
174 repo_name=pull_request.target_repo.scm_instance().name,
175 pull_request_id=pull_request.pull_request_id))
175 pull_request_id=pull_request.pull_request_id))
176
176
177 def test_edit_title_description(self, pr_util, csrf_token):
177 def test_edit_title_description(self, pr_util, csrf_token):
178 pull_request = pr_util.create_pull_request()
178 pull_request = pr_util.create_pull_request()
179 pull_request_id = pull_request.pull_request_id
179 pull_request_id = pull_request.pull_request_id
180
180
181 response = self.app.post(
181 response = self.app.post(
182 route_path('pullrequest_update',
182 route_path('pullrequest_update',
183 repo_name=pull_request.target_repo.repo_name,
183 repo_name=pull_request.target_repo.repo_name,
184 pull_request_id=pull_request_id),
184 pull_request_id=pull_request_id),
185 params={
185 params={
186 'edit_pull_request': 'true',
186 'edit_pull_request': 'true',
187 'title': 'New title',
187 'title': 'New title',
188 'description': 'New description',
188 'description': 'New description',
189 'csrf_token': csrf_token})
189 'csrf_token': csrf_token})
190
190
191 assert_session_flash(
191 assert_session_flash(
192 response, u'Pull request title & description updated.',
192 response, u'Pull request title & description updated.',
193 category='success')
193 category='success')
194
194
195 pull_request = PullRequest.get(pull_request_id)
195 pull_request = PullRequest.get(pull_request_id)
196 assert pull_request.title == 'New title'
196 assert pull_request.title == 'New title'
197 assert pull_request.description == 'New description'
197 assert pull_request.description == 'New description'
198
198
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
200 pull_request = pr_util.create_pull_request()
200 pull_request = pr_util.create_pull_request()
201 pull_request_id = pull_request.pull_request_id
201 pull_request_id = pull_request.pull_request_id
202 pr_util.close()
202 pr_util.close()
203
203
204 response = self.app.post(
204 response = self.app.post(
205 route_path('pullrequest_update',
205 route_path('pullrequest_update',
206 repo_name=pull_request.target_repo.repo_name,
206 repo_name=pull_request.target_repo.repo_name,
207 pull_request_id=pull_request_id),
207 pull_request_id=pull_request_id),
208 params={
208 params={
209 'edit_pull_request': 'true',
209 'edit_pull_request': 'true',
210 'title': 'New title',
210 'title': 'New title',
211 'description': 'New description',
211 'description': 'New description',
212 'csrf_token': csrf_token})
212 'csrf_token': csrf_token})
213
213
214 assert_session_flash(
214 assert_session_flash(
215 response, u'Cannot update closed pull requests.',
215 response, u'Cannot update closed pull requests.',
216 category='error')
216 category='error')
217
217
218 def test_update_invalid_source_reference(self, pr_util, csrf_token):
218 def test_update_invalid_source_reference(self, pr_util, csrf_token):
219 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
219 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
220
220
221 pull_request = pr_util.create_pull_request()
221 pull_request = pr_util.create_pull_request()
222 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
222 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
223 Session().add(pull_request)
223 Session().add(pull_request)
224 Session().commit()
224 Session().commit()
225
225
226 pull_request_id = pull_request.pull_request_id
226 pull_request_id = pull_request.pull_request_id
227
227
228 response = self.app.post(
228 response = self.app.post(
229 route_path('pullrequest_update',
229 route_path('pullrequest_update',
230 repo_name=pull_request.target_repo.repo_name,
230 repo_name=pull_request.target_repo.repo_name,
231 pull_request_id=pull_request_id),
231 pull_request_id=pull_request_id),
232 params={'update_commits': 'true',
232 params={'update_commits': 'true',
233 'csrf_token': csrf_token})
233 'csrf_token': csrf_token})
234
234
235 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
235 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
236 UpdateFailureReason.MISSING_SOURCE_REF]
236 UpdateFailureReason.MISSING_SOURCE_REF])
237 assert_session_flash(response, expected_msg, category='error')
237 assert_session_flash(response, expected_msg, category='error')
238
238
239 def test_missing_target_reference(self, pr_util, csrf_token):
239 def test_missing_target_reference(self, pr_util, csrf_token):
240 from rhodecode.lib.vcs.backends.base import MergeFailureReason
240 from rhodecode.lib.vcs.backends.base import MergeFailureReason
241 pull_request = pr_util.create_pull_request(
241 pull_request = pr_util.create_pull_request(
242 approved=True, mergeable=True)
242 approved=True, mergeable=True)
243 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
243 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
244 Session().add(pull_request)
244 Session().add(pull_request)
245 Session().commit()
245 Session().commit()
246
246
247 pull_request_id = pull_request.pull_request_id
247 pull_request_id = pull_request.pull_request_id
248 pull_request_url = route_path(
248 pull_request_url = route_path(
249 'pullrequest_show',
249 'pullrequest_show',
250 repo_name=pull_request.target_repo.repo_name,
250 repo_name=pull_request.target_repo.repo_name,
251 pull_request_id=pull_request_id)
251 pull_request_id=pull_request_id)
252
252
253 response = self.app.get(pull_request_url)
253 response = self.app.get(pull_request_url)
254
254
255 assertr = AssertResponse(response)
255 assertr = AssertResponse(response)
256 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
256 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
257 MergeFailureReason.MISSING_TARGET_REF]
257 MergeFailureReason.MISSING_TARGET_REF]
258 assertr.element_contains(
258 assertr.element_contains(
259 'span[data-role="merge-message"]', str(expected_msg))
259 'span[data-role="merge-message"]', str(expected_msg))
260
260
261 def test_comment_and_close_pull_request_custom_message_approved(
261 def test_comment_and_close_pull_request_custom_message_approved(
262 self, pr_util, csrf_token, xhr_header):
262 self, pr_util, csrf_token, xhr_header):
263
263
264 pull_request = pr_util.create_pull_request(approved=True)
264 pull_request = pr_util.create_pull_request(approved=True)
265 pull_request_id = pull_request.pull_request_id
265 pull_request_id = pull_request.pull_request_id
266 author = pull_request.user_id
266 author = pull_request.user_id
267 repo = pull_request.target_repo.repo_id
267 repo = pull_request.target_repo.repo_id
268
268
269 self.app.post(
269 self.app.post(
270 route_path('pullrequest_comment_create',
270 route_path('pullrequest_comment_create',
271 repo_name=pull_request.target_repo.scm_instance().name,
271 repo_name=pull_request.target_repo.scm_instance().name,
272 pull_request_id=pull_request_id),
272 pull_request_id=pull_request_id),
273 params={
273 params={
274 'close_pull_request': '1',
274 'close_pull_request': '1',
275 'text': 'Closing a PR',
275 'text': 'Closing a PR',
276 'csrf_token': csrf_token},
276 'csrf_token': csrf_token},
277 extra_environ=xhr_header,)
277 extra_environ=xhr_header,)
278
278
279 journal = UserLog.query()\
279 journal = UserLog.query()\
280 .filter(UserLog.user_id == author)\
280 .filter(UserLog.user_id == author)\
281 .filter(UserLog.repository_id == repo) \
281 .filter(UserLog.repository_id == repo) \
282 .order_by('user_log_id') \
282 .order_by('user_log_id') \
283 .all()
283 .all()
284 assert journal[-1].action == 'repo.pull_request.close'
284 assert journal[-1].action == 'repo.pull_request.close'
285
285
286 pull_request = PullRequest.get(pull_request_id)
286 pull_request = PullRequest.get(pull_request_id)
287 assert pull_request.is_closed()
287 assert pull_request.is_closed()
288
288
289 status = ChangesetStatusModel().get_status(
289 status = ChangesetStatusModel().get_status(
290 pull_request.source_repo, pull_request=pull_request)
290 pull_request.source_repo, pull_request=pull_request)
291 assert status == ChangesetStatus.STATUS_APPROVED
291 assert status == ChangesetStatus.STATUS_APPROVED
292 comments = ChangesetComment().query() \
292 comments = ChangesetComment().query() \
293 .filter(ChangesetComment.pull_request == pull_request) \
293 .filter(ChangesetComment.pull_request == pull_request) \
294 .order_by(ChangesetComment.comment_id.asc())\
294 .order_by(ChangesetComment.comment_id.asc())\
295 .all()
295 .all()
296 assert comments[-1].text == 'Closing a PR'
296 assert comments[-1].text == 'Closing a PR'
297
297
298 def test_comment_force_close_pull_request_rejected(
298 def test_comment_force_close_pull_request_rejected(
299 self, pr_util, csrf_token, xhr_header):
299 self, pr_util, csrf_token, xhr_header):
300 pull_request = pr_util.create_pull_request()
300 pull_request = pr_util.create_pull_request()
301 pull_request_id = pull_request.pull_request_id
301 pull_request_id = pull_request.pull_request_id
302 PullRequestModel().update_reviewers(
302 PullRequestModel().update_reviewers(
303 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
303 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
304 pull_request.author)
304 pull_request.author)
305 author = pull_request.user_id
305 author = pull_request.user_id
306 repo = pull_request.target_repo.repo_id
306 repo = pull_request.target_repo.repo_id
307
307
308 self.app.post(
308 self.app.post(
309 route_path('pullrequest_comment_create',
309 route_path('pullrequest_comment_create',
310 repo_name=pull_request.target_repo.scm_instance().name,
310 repo_name=pull_request.target_repo.scm_instance().name,
311 pull_request_id=pull_request_id),
311 pull_request_id=pull_request_id),
312 params={
312 params={
313 'close_pull_request': '1',
313 'close_pull_request': '1',
314 'csrf_token': csrf_token},
314 'csrf_token': csrf_token},
315 extra_environ=xhr_header)
315 extra_environ=xhr_header)
316
316
317 pull_request = PullRequest.get(pull_request_id)
317 pull_request = PullRequest.get(pull_request_id)
318
318
319 journal = UserLog.query()\
319 journal = UserLog.query()\
320 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
320 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
321 .order_by('user_log_id') \
321 .order_by('user_log_id') \
322 .all()
322 .all()
323 assert journal[-1].action == 'repo.pull_request.close'
323 assert journal[-1].action == 'repo.pull_request.close'
324
324
325 # check only the latest status, not the review status
325 # check only the latest status, not the review status
326 status = ChangesetStatusModel().get_status(
326 status = ChangesetStatusModel().get_status(
327 pull_request.source_repo, pull_request=pull_request)
327 pull_request.source_repo, pull_request=pull_request)
328 assert status == ChangesetStatus.STATUS_REJECTED
328 assert status == ChangesetStatus.STATUS_REJECTED
329
329
330 def test_comment_and_close_pull_request(
330 def test_comment_and_close_pull_request(
331 self, pr_util, csrf_token, xhr_header):
331 self, pr_util, csrf_token, xhr_header):
332 pull_request = pr_util.create_pull_request()
332 pull_request = pr_util.create_pull_request()
333 pull_request_id = pull_request.pull_request_id
333 pull_request_id = pull_request.pull_request_id
334
334
335 response = self.app.post(
335 response = self.app.post(
336 route_path('pullrequest_comment_create',
336 route_path('pullrequest_comment_create',
337 repo_name=pull_request.target_repo.scm_instance().name,
337 repo_name=pull_request.target_repo.scm_instance().name,
338 pull_request_id=pull_request.pull_request_id),
338 pull_request_id=pull_request.pull_request_id),
339 params={
339 params={
340 'close_pull_request': 'true',
340 'close_pull_request': 'true',
341 'csrf_token': csrf_token},
341 'csrf_token': csrf_token},
342 extra_environ=xhr_header)
342 extra_environ=xhr_header)
343
343
344 assert response.json
344 assert response.json
345
345
346 pull_request = PullRequest.get(pull_request_id)
346 pull_request = PullRequest.get(pull_request_id)
347 assert pull_request.is_closed()
347 assert pull_request.is_closed()
348
348
349 # check only the latest status, not the review status
349 # check only the latest status, not the review status
350 status = ChangesetStatusModel().get_status(
350 status = ChangesetStatusModel().get_status(
351 pull_request.source_repo, pull_request=pull_request)
351 pull_request.source_repo, pull_request=pull_request)
352 assert status == ChangesetStatus.STATUS_REJECTED
352 assert status == ChangesetStatus.STATUS_REJECTED
353
353
354 def test_create_pull_request(self, backend, csrf_token):
354 def test_create_pull_request(self, backend, csrf_token):
355 commits = [
355 commits = [
356 {'message': 'ancestor'},
356 {'message': 'ancestor'},
357 {'message': 'change'},
357 {'message': 'change'},
358 {'message': 'change2'},
358 {'message': 'change2'},
359 ]
359 ]
360 commit_ids = backend.create_master_repo(commits)
360 commit_ids = backend.create_master_repo(commits)
361 target = backend.create_repo(heads=['ancestor'])
361 target = backend.create_repo(heads=['ancestor'])
362 source = backend.create_repo(heads=['change2'])
362 source = backend.create_repo(heads=['change2'])
363
363
364 response = self.app.post(
364 response = self.app.post(
365 route_path('pullrequest_create', repo_name=source.repo_name),
365 route_path('pullrequest_create', repo_name=source.repo_name),
366 [
366 [
367 ('source_repo', source.repo_name),
367 ('source_repo', source.repo_name),
368 ('source_ref', 'branch:default:' + commit_ids['change2']),
368 ('source_ref', 'branch:default:' + commit_ids['change2']),
369 ('target_repo', target.repo_name),
369 ('target_repo', target.repo_name),
370 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
370 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
371 ('common_ancestor', commit_ids['ancestor']),
371 ('common_ancestor', commit_ids['ancestor']),
372 ('pullrequest_desc', 'Description'),
372 ('pullrequest_desc', 'Description'),
373 ('pullrequest_title', 'Title'),
373 ('pullrequest_title', 'Title'),
374 ('__start__', 'review_members:sequence'),
374 ('__start__', 'review_members:sequence'),
375 ('__start__', 'reviewer:mapping'),
375 ('__start__', 'reviewer:mapping'),
376 ('user_id', '1'),
376 ('user_id', '1'),
377 ('__start__', 'reasons:sequence'),
377 ('__start__', 'reasons:sequence'),
378 ('reason', 'Some reason'),
378 ('reason', 'Some reason'),
379 ('__end__', 'reasons:sequence'),
379 ('__end__', 'reasons:sequence'),
380 ('mandatory', 'False'),
380 ('mandatory', 'False'),
381 ('__end__', 'reviewer:mapping'),
381 ('__end__', 'reviewer:mapping'),
382 ('__end__', 'review_members:sequence'),
382 ('__end__', 'review_members:sequence'),
383 ('__start__', 'revisions:sequence'),
383 ('__start__', 'revisions:sequence'),
384 ('revisions', commit_ids['change']),
384 ('revisions', commit_ids['change']),
385 ('revisions', commit_ids['change2']),
385 ('revisions', commit_ids['change2']),
386 ('__end__', 'revisions:sequence'),
386 ('__end__', 'revisions:sequence'),
387 ('user', ''),
387 ('user', ''),
388 ('csrf_token', csrf_token),
388 ('csrf_token', csrf_token),
389 ],
389 ],
390 status=302)
390 status=302)
391
391
392 location = response.headers['Location']
392 location = response.headers['Location']
393 pull_request_id = location.rsplit('/', 1)[1]
393 pull_request_id = location.rsplit('/', 1)[1]
394 assert pull_request_id != 'new'
394 assert pull_request_id != 'new'
395 pull_request = PullRequest.get(int(pull_request_id))
395 pull_request = PullRequest.get(int(pull_request_id))
396
396
397 # check that we have now both revisions
397 # check that we have now both revisions
398 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
398 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
399 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
399 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
400 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
400 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
401 assert pull_request.target_ref == expected_target_ref
401 assert pull_request.target_ref == expected_target_ref
402
402
403 def test_reviewer_notifications(self, backend, csrf_token):
403 def test_reviewer_notifications(self, backend, csrf_token):
404 # We have to use the app.post for this test so it will create the
404 # We have to use the app.post for this test so it will create the
405 # notifications properly with the new PR
405 # notifications properly with the new PR
406 commits = [
406 commits = [
407 {'message': 'ancestor',
407 {'message': 'ancestor',
408 'added': [FileNode('file_A', content='content_of_ancestor')]},
408 'added': [FileNode('file_A', content='content_of_ancestor')]},
409 {'message': 'change',
409 {'message': 'change',
410 'added': [FileNode('file_a', content='content_of_change')]},
410 'added': [FileNode('file_a', content='content_of_change')]},
411 {'message': 'change-child'},
411 {'message': 'change-child'},
412 {'message': 'ancestor-child', 'parents': ['ancestor'],
412 {'message': 'ancestor-child', 'parents': ['ancestor'],
413 'added': [
413 'added': [
414 FileNode('file_B', content='content_of_ancestor_child')]},
414 FileNode('file_B', content='content_of_ancestor_child')]},
415 {'message': 'ancestor-child-2'},
415 {'message': 'ancestor-child-2'},
416 ]
416 ]
417 commit_ids = backend.create_master_repo(commits)
417 commit_ids = backend.create_master_repo(commits)
418 target = backend.create_repo(heads=['ancestor-child'])
418 target = backend.create_repo(heads=['ancestor-child'])
419 source = backend.create_repo(heads=['change'])
419 source = backend.create_repo(heads=['change'])
420
420
421 response = self.app.post(
421 response = self.app.post(
422 route_path('pullrequest_create', repo_name=source.repo_name),
422 route_path('pullrequest_create', repo_name=source.repo_name),
423 [
423 [
424 ('source_repo', source.repo_name),
424 ('source_repo', source.repo_name),
425 ('source_ref', 'branch:default:' + commit_ids['change']),
425 ('source_ref', 'branch:default:' + commit_ids['change']),
426 ('target_repo', target.repo_name),
426 ('target_repo', target.repo_name),
427 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
427 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
428 ('common_ancestor', commit_ids['ancestor']),
428 ('common_ancestor', commit_ids['ancestor']),
429 ('pullrequest_desc', 'Description'),
429 ('pullrequest_desc', 'Description'),
430 ('pullrequest_title', 'Title'),
430 ('pullrequest_title', 'Title'),
431 ('__start__', 'review_members:sequence'),
431 ('__start__', 'review_members:sequence'),
432 ('__start__', 'reviewer:mapping'),
432 ('__start__', 'reviewer:mapping'),
433 ('user_id', '2'),
433 ('user_id', '2'),
434 ('__start__', 'reasons:sequence'),
434 ('__start__', 'reasons:sequence'),
435 ('reason', 'Some reason'),
435 ('reason', 'Some reason'),
436 ('__end__', 'reasons:sequence'),
436 ('__end__', 'reasons:sequence'),
437 ('mandatory', 'False'),
437 ('mandatory', 'False'),
438 ('__end__', 'reviewer:mapping'),
438 ('__end__', 'reviewer:mapping'),
439 ('__end__', 'review_members:sequence'),
439 ('__end__', 'review_members:sequence'),
440 ('__start__', 'revisions:sequence'),
440 ('__start__', 'revisions:sequence'),
441 ('revisions', commit_ids['change']),
441 ('revisions', commit_ids['change']),
442 ('__end__', 'revisions:sequence'),
442 ('__end__', 'revisions:sequence'),
443 ('user', ''),
443 ('user', ''),
444 ('csrf_token', csrf_token),
444 ('csrf_token', csrf_token),
445 ],
445 ],
446 status=302)
446 status=302)
447
447
448 location = response.headers['Location']
448 location = response.headers['Location']
449
449
450 pull_request_id = location.rsplit('/', 1)[1]
450 pull_request_id = location.rsplit('/', 1)[1]
451 assert pull_request_id != 'new'
451 assert pull_request_id != 'new'
452 pull_request = PullRequest.get(int(pull_request_id))
452 pull_request = PullRequest.get(int(pull_request_id))
453
453
454 # Check that a notification was made
454 # Check that a notification was made
455 notifications = Notification.query()\
455 notifications = Notification.query()\
456 .filter(Notification.created_by == pull_request.author.user_id,
456 .filter(Notification.created_by == pull_request.author.user_id,
457 Notification.type_ == Notification.TYPE_PULL_REQUEST,
457 Notification.type_ == Notification.TYPE_PULL_REQUEST,
458 Notification.subject.contains(
458 Notification.subject.contains(
459 "wants you to review pull request #%s" % pull_request_id))
459 "wants you to review pull request #%s" % pull_request_id))
460 assert len(notifications.all()) == 1
460 assert len(notifications.all()) == 1
461
461
462 # Change reviewers and check that a notification was made
462 # Change reviewers and check that a notification was made
463 PullRequestModel().update_reviewers(
463 PullRequestModel().update_reviewers(
464 pull_request.pull_request_id, [(1, [], False)],
464 pull_request.pull_request_id, [(1, [], False)],
465 pull_request.author)
465 pull_request.author)
466 assert len(notifications.all()) == 2
466 assert len(notifications.all()) == 2
467
467
468 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
468 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
469 csrf_token):
469 csrf_token):
470 commits = [
470 commits = [
471 {'message': 'ancestor',
471 {'message': 'ancestor',
472 'added': [FileNode('file_A', content='content_of_ancestor')]},
472 'added': [FileNode('file_A', content='content_of_ancestor')]},
473 {'message': 'change',
473 {'message': 'change',
474 'added': [FileNode('file_a', content='content_of_change')]},
474 'added': [FileNode('file_a', content='content_of_change')]},
475 {'message': 'change-child'},
475 {'message': 'change-child'},
476 {'message': 'ancestor-child', 'parents': ['ancestor'],
476 {'message': 'ancestor-child', 'parents': ['ancestor'],
477 'added': [
477 'added': [
478 FileNode('file_B', content='content_of_ancestor_child')]},
478 FileNode('file_B', content='content_of_ancestor_child')]},
479 {'message': 'ancestor-child-2'},
479 {'message': 'ancestor-child-2'},
480 ]
480 ]
481 commit_ids = backend.create_master_repo(commits)
481 commit_ids = backend.create_master_repo(commits)
482 target = backend.create_repo(heads=['ancestor-child'])
482 target = backend.create_repo(heads=['ancestor-child'])
483 source = backend.create_repo(heads=['change'])
483 source = backend.create_repo(heads=['change'])
484
484
485 response = self.app.post(
485 response = self.app.post(
486 route_path('pullrequest_create', repo_name=source.repo_name),
486 route_path('pullrequest_create', repo_name=source.repo_name),
487 [
487 [
488 ('source_repo', source.repo_name),
488 ('source_repo', source.repo_name),
489 ('source_ref', 'branch:default:' + commit_ids['change']),
489 ('source_ref', 'branch:default:' + commit_ids['change']),
490 ('target_repo', target.repo_name),
490 ('target_repo', target.repo_name),
491 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
491 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
492 ('common_ancestor', commit_ids['ancestor']),
492 ('common_ancestor', commit_ids['ancestor']),
493 ('pullrequest_desc', 'Description'),
493 ('pullrequest_desc', 'Description'),
494 ('pullrequest_title', 'Title'),
494 ('pullrequest_title', 'Title'),
495 ('__start__', 'review_members:sequence'),
495 ('__start__', 'review_members:sequence'),
496 ('__start__', 'reviewer:mapping'),
496 ('__start__', 'reviewer:mapping'),
497 ('user_id', '1'),
497 ('user_id', '1'),
498 ('__start__', 'reasons:sequence'),
498 ('__start__', 'reasons:sequence'),
499 ('reason', 'Some reason'),
499 ('reason', 'Some reason'),
500 ('__end__', 'reasons:sequence'),
500 ('__end__', 'reasons:sequence'),
501 ('mandatory', 'False'),
501 ('mandatory', 'False'),
502 ('__end__', 'reviewer:mapping'),
502 ('__end__', 'reviewer:mapping'),
503 ('__end__', 'review_members:sequence'),
503 ('__end__', 'review_members:sequence'),
504 ('__start__', 'revisions:sequence'),
504 ('__start__', 'revisions:sequence'),
505 ('revisions', commit_ids['change']),
505 ('revisions', commit_ids['change']),
506 ('__end__', 'revisions:sequence'),
506 ('__end__', 'revisions:sequence'),
507 ('user', ''),
507 ('user', ''),
508 ('csrf_token', csrf_token),
508 ('csrf_token', csrf_token),
509 ],
509 ],
510 status=302)
510 status=302)
511
511
512 location = response.headers['Location']
512 location = response.headers['Location']
513
513
514 pull_request_id = location.rsplit('/', 1)[1]
514 pull_request_id = location.rsplit('/', 1)[1]
515 assert pull_request_id != 'new'
515 assert pull_request_id != 'new'
516 pull_request = PullRequest.get(int(pull_request_id))
516 pull_request = PullRequest.get(int(pull_request_id))
517
517
518 # target_ref has to point to the ancestor's commit_id in order to
518 # target_ref has to point to the ancestor's commit_id in order to
519 # show the correct diff
519 # show the correct diff
520 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
520 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
521 assert pull_request.target_ref == expected_target_ref
521 assert pull_request.target_ref == expected_target_ref
522
522
523 # Check generated diff contents
523 # Check generated diff contents
524 response = response.follow()
524 response = response.follow()
525 assert 'content_of_ancestor' not in response.body
525 assert 'content_of_ancestor' not in response.body
526 assert 'content_of_ancestor-child' not in response.body
526 assert 'content_of_ancestor-child' not in response.body
527 assert 'content_of_change' in response.body
527 assert 'content_of_change' in response.body
528
528
529 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
529 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
530 # Clear any previous calls to rcextensions
530 # Clear any previous calls to rcextensions
531 rhodecode.EXTENSIONS.calls.clear()
531 rhodecode.EXTENSIONS.calls.clear()
532
532
533 pull_request = pr_util.create_pull_request(
533 pull_request = pr_util.create_pull_request(
534 approved=True, mergeable=True)
534 approved=True, mergeable=True)
535 pull_request_id = pull_request.pull_request_id
535 pull_request_id = pull_request.pull_request_id
536 repo_name = pull_request.target_repo.scm_instance().name,
536 repo_name = pull_request.target_repo.scm_instance().name,
537
537
538 response = self.app.post(
538 response = self.app.post(
539 route_path('pullrequest_merge',
539 route_path('pullrequest_merge',
540 repo_name=str(repo_name[0]),
540 repo_name=str(repo_name[0]),
541 pull_request_id=pull_request_id),
541 pull_request_id=pull_request_id),
542 params={'csrf_token': csrf_token}).follow()
542 params={'csrf_token': csrf_token}).follow()
543
543
544 pull_request = PullRequest.get(pull_request_id)
544 pull_request = PullRequest.get(pull_request_id)
545
545
546 assert response.status_int == 200
546 assert response.status_int == 200
547 assert pull_request.is_closed()
547 assert pull_request.is_closed()
548 assert_pull_request_status(
548 assert_pull_request_status(
549 pull_request, ChangesetStatus.STATUS_APPROVED)
549 pull_request, ChangesetStatus.STATUS_APPROVED)
550
550
551 # Check the relevant log entries were added
551 # Check the relevant log entries were added
552 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
552 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
553 actions = [log.action for log in user_logs]
553 actions = [log.action for log in user_logs]
554 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
554 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
555 expected_actions = [
555 expected_actions = [
556 u'repo.pull_request.close',
556 u'repo.pull_request.close',
557 u'repo.pull_request.merge',
557 u'repo.pull_request.merge',
558 u'repo.pull_request.comment.create'
558 u'repo.pull_request.comment.create'
559 ]
559 ]
560 assert actions == expected_actions
560 assert actions == expected_actions
561
561
562 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
562 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
563 actions = [log for log in user_logs]
563 actions = [log for log in user_logs]
564 assert actions[-1].action == 'user.push'
564 assert actions[-1].action == 'user.push'
565 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
565 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
566
566
567 # Check post_push rcextension was really executed
567 # Check post_push rcextension was really executed
568 push_calls = rhodecode.EXTENSIONS.calls['post_push']
568 push_calls = rhodecode.EXTENSIONS.calls['post_push']
569 assert len(push_calls) == 1
569 assert len(push_calls) == 1
570 unused_last_call_args, last_call_kwargs = push_calls[0]
570 unused_last_call_args, last_call_kwargs = push_calls[0]
571 assert last_call_kwargs['action'] == 'push'
571 assert last_call_kwargs['action'] == 'push'
572 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
572 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
573
573
574 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
574 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
575 pull_request = pr_util.create_pull_request(mergeable=False)
575 pull_request = pr_util.create_pull_request(mergeable=False)
576 pull_request_id = pull_request.pull_request_id
576 pull_request_id = pull_request.pull_request_id
577 pull_request = PullRequest.get(pull_request_id)
577 pull_request = PullRequest.get(pull_request_id)
578
578
579 response = self.app.post(
579 response = self.app.post(
580 route_path('pullrequest_merge',
580 route_path('pullrequest_merge',
581 repo_name=pull_request.target_repo.scm_instance().name,
581 repo_name=pull_request.target_repo.scm_instance().name,
582 pull_request_id=pull_request.pull_request_id),
582 pull_request_id=pull_request.pull_request_id),
583 params={'csrf_token': csrf_token}).follow()
583 params={'csrf_token': csrf_token}).follow()
584
584
585 assert response.status_int == 200
585 assert response.status_int == 200
586 response.mustcontain(
586 response.mustcontain(
587 'Merge is not currently possible because of below failed checks.')
587 'Merge is not currently possible because of below failed checks.')
588 response.mustcontain('Server-side pull request merging is disabled.')
588 response.mustcontain('Server-side pull request merging is disabled.')
589
589
590 @pytest.mark.skip_backends('svn')
590 @pytest.mark.skip_backends('svn')
591 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
591 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
592 pull_request = pr_util.create_pull_request(mergeable=True)
592 pull_request = pr_util.create_pull_request(mergeable=True)
593 pull_request_id = pull_request.pull_request_id
593 pull_request_id = pull_request.pull_request_id
594 repo_name = pull_request.target_repo.scm_instance().name
594 repo_name = pull_request.target_repo.scm_instance().name
595
595
596 response = self.app.post(
596 response = self.app.post(
597 route_path('pullrequest_merge',
597 route_path('pullrequest_merge',
598 repo_name=repo_name,
598 repo_name=repo_name,
599 pull_request_id=pull_request_id),
599 pull_request_id=pull_request_id),
600 params={'csrf_token': csrf_token}).follow()
600 params={'csrf_token': csrf_token}).follow()
601
601
602 assert response.status_int == 200
602 assert response.status_int == 200
603
603
604 response.mustcontain(
604 response.mustcontain(
605 'Merge is not currently possible because of below failed checks.')
605 'Merge is not currently possible because of below failed checks.')
606 response.mustcontain('Pull request reviewer approval is pending.')
606 response.mustcontain('Pull request reviewer approval is pending.')
607
607
608 def test_merge_pull_request_renders_failure_reason(
608 def test_merge_pull_request_renders_failure_reason(
609 self, user_regular, csrf_token, pr_util):
609 self, user_regular, csrf_token, pr_util):
610 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
610 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
611 pull_request_id = pull_request.pull_request_id
611 pull_request_id = pull_request.pull_request_id
612 repo_name = pull_request.target_repo.scm_instance().name
612 repo_name = pull_request.target_repo.scm_instance().name
613
613
614 model_patcher = mock.patch.multiple(
614 model_patcher = mock.patch.multiple(
615 PullRequestModel,
615 PullRequestModel,
616 merge=mock.Mock(return_value=MergeResponse(
616 merge=mock.Mock(return_value=MergeResponse(
617 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
617 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
618 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
618 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
619
619
620 with model_patcher:
620 with model_patcher:
621 response = self.app.post(
621 response = self.app.post(
622 route_path('pullrequest_merge',
622 route_path('pullrequest_merge',
623 repo_name=repo_name,
623 repo_name=repo_name,
624 pull_request_id=pull_request_id),
624 pull_request_id=pull_request_id),
625 params={'csrf_token': csrf_token}, status=302)
625 params={'csrf_token': csrf_token}, status=302)
626
626
627 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
627 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
628 MergeFailureReason.PUSH_FAILED])
628 MergeFailureReason.PUSH_FAILED])
629
629
630 def test_update_source_revision(self, backend, csrf_token):
630 def test_update_source_revision(self, backend, csrf_token):
631 commits = [
631 commits = [
632 {'message': 'ancestor'},
632 {'message': 'ancestor'},
633 {'message': 'change'},
633 {'message': 'change'},
634 {'message': 'change-2'},
634 {'message': 'change-2'},
635 ]
635 ]
636 commit_ids = backend.create_master_repo(commits)
636 commit_ids = backend.create_master_repo(commits)
637 target = backend.create_repo(heads=['ancestor'])
637 target = backend.create_repo(heads=['ancestor'])
638 source = backend.create_repo(heads=['change'])
638 source = backend.create_repo(heads=['change'])
639
639
640 # create pr from a in source to A in target
640 # create pr from a in source to A in target
641 pull_request = PullRequest()
641 pull_request = PullRequest()
642 pull_request.source_repo = source
642 pull_request.source_repo = source
643 # TODO: johbo: Make sure that we write the source ref this way!
643 # TODO: johbo: Make sure that we write the source ref this way!
644 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
644 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
645 branch=backend.default_branch_name, commit_id=commit_ids['change'])
645 branch=backend.default_branch_name, commit_id=commit_ids['change'])
646 pull_request.target_repo = target
646 pull_request.target_repo = target
647
647
648 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
648 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
649 branch=backend.default_branch_name,
649 branch=backend.default_branch_name,
650 commit_id=commit_ids['ancestor'])
650 commit_id=commit_ids['ancestor'])
651 pull_request.revisions = [commit_ids['change']]
651 pull_request.revisions = [commit_ids['change']]
652 pull_request.title = u"Test"
652 pull_request.title = u"Test"
653 pull_request.description = u"Description"
653 pull_request.description = u"Description"
654 pull_request.author = UserModel().get_by_username(
654 pull_request.author = UserModel().get_by_username(
655 TEST_USER_ADMIN_LOGIN)
655 TEST_USER_ADMIN_LOGIN)
656 Session().add(pull_request)
656 Session().add(pull_request)
657 Session().commit()
657 Session().commit()
658 pull_request_id = pull_request.pull_request_id
658 pull_request_id = pull_request.pull_request_id
659
659
660 # source has ancestor - change - change-2
660 # source has ancestor - change - change-2
661 backend.pull_heads(source, heads=['change-2'])
661 backend.pull_heads(source, heads=['change-2'])
662
662
663 # update PR
663 # update PR
664 self.app.post(
664 self.app.post(
665 route_path('pullrequest_update',
665 route_path('pullrequest_update',
666 repo_name=target.repo_name,
666 repo_name=target.repo_name,
667 pull_request_id=pull_request_id),
667 pull_request_id=pull_request_id),
668 params={'update_commits': 'true',
668 params={'update_commits': 'true',
669 'csrf_token': csrf_token})
669 'csrf_token': csrf_token})
670
670
671 # check that we have now both revisions
671 # check that we have now both revisions
672 pull_request = PullRequest.get(pull_request_id)
672 pull_request = PullRequest.get(pull_request_id)
673 assert pull_request.revisions == [
673 assert pull_request.revisions == [
674 commit_ids['change-2'], commit_ids['change']]
674 commit_ids['change-2'], commit_ids['change']]
675
675
676 # TODO: johbo: this should be a test on its own
676 # TODO: johbo: this should be a test on its own
677 response = self.app.get(route_path(
677 response = self.app.get(route_path(
678 'pullrequest_new',
678 'pullrequest_new',
679 repo_name=target.repo_name))
679 repo_name=target.repo_name))
680 assert response.status_int == 200
680 assert response.status_int == 200
681 assert 'Pull request updated to' in response.body
681 assert 'Pull request updated to' in response.body
682 assert 'with 1 added, 0 removed commits.' in response.body
682 assert 'with 1 added, 0 removed commits.' in response.body
683
683
684 def test_update_target_revision(self, backend, csrf_token):
684 def test_update_target_revision(self, backend, csrf_token):
685 commits = [
685 commits = [
686 {'message': 'ancestor'},
686 {'message': 'ancestor'},
687 {'message': 'change'},
687 {'message': 'change'},
688 {'message': 'ancestor-new', 'parents': ['ancestor']},
688 {'message': 'ancestor-new', 'parents': ['ancestor']},
689 {'message': 'change-rebased'},
689 {'message': 'change-rebased'},
690 ]
690 ]
691 commit_ids = backend.create_master_repo(commits)
691 commit_ids = backend.create_master_repo(commits)
692 target = backend.create_repo(heads=['ancestor'])
692 target = backend.create_repo(heads=['ancestor'])
693 source = backend.create_repo(heads=['change'])
693 source = backend.create_repo(heads=['change'])
694
694
695 # create pr from a in source to A in target
695 # create pr from a in source to A in target
696 pull_request = PullRequest()
696 pull_request = PullRequest()
697 pull_request.source_repo = source
697 pull_request.source_repo = source
698 # TODO: johbo: Make sure that we write the source ref this way!
698 # TODO: johbo: Make sure that we write the source ref this way!
699 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
699 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
700 branch=backend.default_branch_name, commit_id=commit_ids['change'])
700 branch=backend.default_branch_name, commit_id=commit_ids['change'])
701 pull_request.target_repo = target
701 pull_request.target_repo = target
702 # TODO: johbo: Target ref should be branch based, since tip can jump
702 # TODO: johbo: Target ref should be branch based, since tip can jump
703 # from branch to branch
703 # from branch to branch
704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
705 branch=backend.default_branch_name,
705 branch=backend.default_branch_name,
706 commit_id=commit_ids['ancestor'])
706 commit_id=commit_ids['ancestor'])
707 pull_request.revisions = [commit_ids['change']]
707 pull_request.revisions = [commit_ids['change']]
708 pull_request.title = u"Test"
708 pull_request.title = u"Test"
709 pull_request.description = u"Description"
709 pull_request.description = u"Description"
710 pull_request.author = UserModel().get_by_username(
710 pull_request.author = UserModel().get_by_username(
711 TEST_USER_ADMIN_LOGIN)
711 TEST_USER_ADMIN_LOGIN)
712 Session().add(pull_request)
712 Session().add(pull_request)
713 Session().commit()
713 Session().commit()
714 pull_request_id = pull_request.pull_request_id
714 pull_request_id = pull_request.pull_request_id
715
715
716 # target has ancestor - ancestor-new
716 # target has ancestor - ancestor-new
717 # source has ancestor - ancestor-new - change-rebased
717 # source has ancestor - ancestor-new - change-rebased
718 backend.pull_heads(target, heads=['ancestor-new'])
718 backend.pull_heads(target, heads=['ancestor-new'])
719 backend.pull_heads(source, heads=['change-rebased'])
719 backend.pull_heads(source, heads=['change-rebased'])
720
720
721 # update PR
721 # update PR
722 self.app.post(
722 self.app.post(
723 route_path('pullrequest_update',
723 route_path('pullrequest_update',
724 repo_name=target.repo_name,
724 repo_name=target.repo_name,
725 pull_request_id=pull_request_id),
725 pull_request_id=pull_request_id),
726 params={'update_commits': 'true',
726 params={'update_commits': 'true',
727 'csrf_token': csrf_token},
727 'csrf_token': csrf_token},
728 status=200)
728 status=200)
729
729
730 # check that we have now both revisions
730 # check that we have now both revisions
731 pull_request = PullRequest.get(pull_request_id)
731 pull_request = PullRequest.get(pull_request_id)
732 assert pull_request.revisions == [commit_ids['change-rebased']]
732 assert pull_request.revisions == [commit_ids['change-rebased']]
733 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
733 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
734 branch=backend.default_branch_name,
734 branch=backend.default_branch_name,
735 commit_id=commit_ids['ancestor-new'])
735 commit_id=commit_ids['ancestor-new'])
736
736
737 # TODO: johbo: This should be a test on its own
737 # TODO: johbo: This should be a test on its own
738 response = self.app.get(route_path(
738 response = self.app.get(route_path(
739 'pullrequest_new',
739 'pullrequest_new',
740 repo_name=target.repo_name))
740 repo_name=target.repo_name))
741 assert response.status_int == 200
741 assert response.status_int == 200
742 assert 'Pull request updated to' in response.body
742 assert 'Pull request updated to' in response.body
743 assert 'with 1 added, 1 removed commits.' in response.body
743 assert 'with 1 added, 1 removed commits.' in response.body
744
744
745 def test_update_of_ancestor_reference(self, backend, csrf_token):
745 def test_update_of_ancestor_reference(self, backend, csrf_token):
746 commits = [
746 commits = [
747 {'message': 'ancestor'},
747 {'message': 'ancestor'},
748 {'message': 'change'},
748 {'message': 'change'},
749 {'message': 'change-2'},
749 {'message': 'change-2'},
750 {'message': 'ancestor-new', 'parents': ['ancestor']},
750 {'message': 'ancestor-new', 'parents': ['ancestor']},
751 {'message': 'change-rebased'},
751 {'message': 'change-rebased'},
752 ]
752 ]
753 commit_ids = backend.create_master_repo(commits)
753 commit_ids = backend.create_master_repo(commits)
754 target = backend.create_repo(heads=['ancestor'])
754 target = backend.create_repo(heads=['ancestor'])
755 source = backend.create_repo(heads=['change'])
755 source = backend.create_repo(heads=['change'])
756
756
757 # create pr from a in source to A in target
757 # create pr from a in source to A in target
758 pull_request = PullRequest()
758 pull_request = PullRequest()
759 pull_request.source_repo = source
759 pull_request.source_repo = source
760 # TODO: johbo: Make sure that we write the source ref this way!
760 # TODO: johbo: Make sure that we write the source ref this way!
761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
762 branch=backend.default_branch_name,
762 branch=backend.default_branch_name,
763 commit_id=commit_ids['change'])
763 commit_id=commit_ids['change'])
764 pull_request.target_repo = target
764 pull_request.target_repo = target
765 # TODO: johbo: Target ref should be branch based, since tip can jump
765 # TODO: johbo: Target ref should be branch based, since tip can jump
766 # from branch to branch
766 # from branch to branch
767 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
767 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
768 branch=backend.default_branch_name,
768 branch=backend.default_branch_name,
769 commit_id=commit_ids['ancestor'])
769 commit_id=commit_ids['ancestor'])
770 pull_request.revisions = [commit_ids['change']]
770 pull_request.revisions = [commit_ids['change']]
771 pull_request.title = u"Test"
771 pull_request.title = u"Test"
772 pull_request.description = u"Description"
772 pull_request.description = u"Description"
773 pull_request.author = UserModel().get_by_username(
773 pull_request.author = UserModel().get_by_username(
774 TEST_USER_ADMIN_LOGIN)
774 TEST_USER_ADMIN_LOGIN)
775 Session().add(pull_request)
775 Session().add(pull_request)
776 Session().commit()
776 Session().commit()
777 pull_request_id = pull_request.pull_request_id
777 pull_request_id = pull_request.pull_request_id
778
778
779 # target has ancestor - ancestor-new
779 # target has ancestor - ancestor-new
780 # source has ancestor - ancestor-new - change-rebased
780 # source has ancestor - ancestor-new - change-rebased
781 backend.pull_heads(target, heads=['ancestor-new'])
781 backend.pull_heads(target, heads=['ancestor-new'])
782 backend.pull_heads(source, heads=['change-rebased'])
782 backend.pull_heads(source, heads=['change-rebased'])
783
783
784 # update PR
784 # update PR
785 self.app.post(
785 self.app.post(
786 route_path('pullrequest_update',
786 route_path('pullrequest_update',
787 repo_name=target.repo_name,
787 repo_name=target.repo_name,
788 pull_request_id=pull_request_id),
788 pull_request_id=pull_request_id),
789 params={'update_commits': 'true',
789 params={'update_commits': 'true',
790 'csrf_token': csrf_token},
790 'csrf_token': csrf_token},
791 status=200)
791 status=200)
792
792
793 # Expect the target reference to be updated correctly
793 # Expect the target reference to be updated correctly
794 pull_request = PullRequest.get(pull_request_id)
794 pull_request = PullRequest.get(pull_request_id)
795 assert pull_request.revisions == [commit_ids['change-rebased']]
795 assert pull_request.revisions == [commit_ids['change-rebased']]
796 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
796 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
797 branch=backend.default_branch_name,
797 branch=backend.default_branch_name,
798 commit_id=commit_ids['ancestor-new'])
798 commit_id=commit_ids['ancestor-new'])
799 assert pull_request.target_ref == expected_target_ref
799 assert pull_request.target_ref == expected_target_ref
800
800
801 def test_remove_pull_request_branch(self, backend_git, csrf_token):
801 def test_remove_pull_request_branch(self, backend_git, csrf_token):
802 branch_name = 'development'
802 branch_name = 'development'
803 commits = [
803 commits = [
804 {'message': 'initial-commit'},
804 {'message': 'initial-commit'},
805 {'message': 'old-feature'},
805 {'message': 'old-feature'},
806 {'message': 'new-feature', 'branch': branch_name},
806 {'message': 'new-feature', 'branch': branch_name},
807 ]
807 ]
808 repo = backend_git.create_repo(commits)
808 repo = backend_git.create_repo(commits)
809 commit_ids = backend_git.commit_ids
809 commit_ids = backend_git.commit_ids
810
810
811 pull_request = PullRequest()
811 pull_request = PullRequest()
812 pull_request.source_repo = repo
812 pull_request.source_repo = repo
813 pull_request.target_repo = repo
813 pull_request.target_repo = repo
814 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
814 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
815 branch=branch_name, commit_id=commit_ids['new-feature'])
815 branch=branch_name, commit_id=commit_ids['new-feature'])
816 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
816 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
817 branch=backend_git.default_branch_name,
817 branch=backend_git.default_branch_name,
818 commit_id=commit_ids['old-feature'])
818 commit_id=commit_ids['old-feature'])
819 pull_request.revisions = [commit_ids['new-feature']]
819 pull_request.revisions = [commit_ids['new-feature']]
820 pull_request.title = u"Test"
820 pull_request.title = u"Test"
821 pull_request.description = u"Description"
821 pull_request.description = u"Description"
822 pull_request.author = UserModel().get_by_username(
822 pull_request.author = UserModel().get_by_username(
823 TEST_USER_ADMIN_LOGIN)
823 TEST_USER_ADMIN_LOGIN)
824 Session().add(pull_request)
824 Session().add(pull_request)
825 Session().commit()
825 Session().commit()
826
826
827 vcs = repo.scm_instance()
827 vcs = repo.scm_instance()
828 vcs.remove_ref('refs/heads/{}'.format(branch_name))
828 vcs.remove_ref('refs/heads/{}'.format(branch_name))
829
829
830 response = self.app.get(route_path(
830 response = self.app.get(route_path(
831 'pullrequest_show',
831 'pullrequest_show',
832 repo_name=repo.repo_name,
832 repo_name=repo.repo_name,
833 pull_request_id=pull_request.pull_request_id))
833 pull_request_id=pull_request.pull_request_id))
834
834
835 assert response.status_int == 200
835 assert response.status_int == 200
836 assert_response = AssertResponse(response)
836 assert_response = AssertResponse(response)
837 assert_response.element_contains(
837 assert_response.element_contains(
838 '#changeset_compare_view_content .alert strong',
838 '#changeset_compare_view_content .alert strong',
839 'Missing commits')
839 'Missing commits')
840 assert_response.element_contains(
840 assert_response.element_contains(
841 '#changeset_compare_view_content .alert',
841 '#changeset_compare_view_content .alert',
842 'This pull request cannot be displayed, because one or more'
842 'This pull request cannot be displayed, because one or more'
843 ' commits no longer exist in the source repository.')
843 ' commits no longer exist in the source repository.')
844
844
845 def test_strip_commits_from_pull_request(
845 def test_strip_commits_from_pull_request(
846 self, backend, pr_util, csrf_token):
846 self, backend, pr_util, csrf_token):
847 commits = [
847 commits = [
848 {'message': 'initial-commit'},
848 {'message': 'initial-commit'},
849 {'message': 'old-feature'},
849 {'message': 'old-feature'},
850 {'message': 'new-feature', 'parents': ['initial-commit']},
850 {'message': 'new-feature', 'parents': ['initial-commit']},
851 ]
851 ]
852 pull_request = pr_util.create_pull_request(
852 pull_request = pr_util.create_pull_request(
853 commits, target_head='initial-commit', source_head='new-feature',
853 commits, target_head='initial-commit', source_head='new-feature',
854 revisions=['new-feature'])
854 revisions=['new-feature'])
855
855
856 vcs = pr_util.source_repository.scm_instance()
856 vcs = pr_util.source_repository.scm_instance()
857 if backend.alias == 'git':
857 if backend.alias == 'git':
858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
859 else:
859 else:
860 vcs.strip(pr_util.commit_ids['new-feature'])
860 vcs.strip(pr_util.commit_ids['new-feature'])
861
861
862 response = self.app.get(route_path(
862 response = self.app.get(route_path(
863 'pullrequest_show',
863 'pullrequest_show',
864 repo_name=pr_util.target_repository.repo_name,
864 repo_name=pr_util.target_repository.repo_name,
865 pull_request_id=pull_request.pull_request_id))
865 pull_request_id=pull_request.pull_request_id))
866
866
867 assert response.status_int == 200
867 assert response.status_int == 200
868 assert_response = AssertResponse(response)
868 assert_response = AssertResponse(response)
869 assert_response.element_contains(
869 assert_response.element_contains(
870 '#changeset_compare_view_content .alert strong',
870 '#changeset_compare_view_content .alert strong',
871 'Missing commits')
871 'Missing commits')
872 assert_response.element_contains(
872 assert_response.element_contains(
873 '#changeset_compare_view_content .alert',
873 '#changeset_compare_view_content .alert',
874 'This pull request cannot be displayed, because one or more'
874 'This pull request cannot be displayed, because one or more'
875 ' commits no longer exist in the source repository.')
875 ' commits no longer exist in the source repository.')
876 assert_response.element_contains(
876 assert_response.element_contains(
877 '#update_commits',
877 '#update_commits',
878 'Update commits')
878 'Update commits')
879
879
880 def test_strip_commits_and_update(
880 def test_strip_commits_and_update(
881 self, backend, pr_util, csrf_token):
881 self, backend, pr_util, csrf_token):
882 commits = [
882 commits = [
883 {'message': 'initial-commit'},
883 {'message': 'initial-commit'},
884 {'message': 'old-feature'},
884 {'message': 'old-feature'},
885 {'message': 'new-feature', 'parents': ['old-feature']},
885 {'message': 'new-feature', 'parents': ['old-feature']},
886 ]
886 ]
887 pull_request = pr_util.create_pull_request(
887 pull_request = pr_util.create_pull_request(
888 commits, target_head='old-feature', source_head='new-feature',
888 commits, target_head='old-feature', source_head='new-feature',
889 revisions=['new-feature'], mergeable=True)
889 revisions=['new-feature'], mergeable=True)
890
890
891 vcs = pr_util.source_repository.scm_instance()
891 vcs = pr_util.source_repository.scm_instance()
892 if backend.alias == 'git':
892 if backend.alias == 'git':
893 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
893 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
894 else:
894 else:
895 vcs.strip(pr_util.commit_ids['new-feature'])
895 vcs.strip(pr_util.commit_ids['new-feature'])
896
896
897 response = self.app.post(
897 response = self.app.post(
898 route_path('pullrequest_update',
898 route_path('pullrequest_update',
899 repo_name=pull_request.target_repo.repo_name,
899 repo_name=pull_request.target_repo.repo_name,
900 pull_request_id=pull_request.pull_request_id),
900 pull_request_id=pull_request.pull_request_id),
901 params={'update_commits': 'true',
901 params={'update_commits': 'true',
902 'csrf_token': csrf_token})
902 'csrf_token': csrf_token})
903
903
904 assert response.status_int == 200
904 assert response.status_int == 200
905 assert response.body == 'true'
905 assert response.body == 'true'
906
906
907 # Make sure that after update, it won't raise 500 errors
907 # Make sure that after update, it won't raise 500 errors
908 response = self.app.get(route_path(
908 response = self.app.get(route_path(
909 'pullrequest_show',
909 'pullrequest_show',
910 repo_name=pr_util.target_repository.repo_name,
910 repo_name=pr_util.target_repository.repo_name,
911 pull_request_id=pull_request.pull_request_id))
911 pull_request_id=pull_request.pull_request_id))
912
912
913 assert response.status_int == 200
913 assert response.status_int == 200
914 assert_response = AssertResponse(response)
914 assert_response = AssertResponse(response)
915 assert_response.element_contains(
915 assert_response.element_contains(
916 '#changeset_compare_view_content .alert strong',
916 '#changeset_compare_view_content .alert strong',
917 'Missing commits')
917 'Missing commits')
918
918
919 def test_branch_is_a_link(self, pr_util):
919 def test_branch_is_a_link(self, pr_util):
920 pull_request = pr_util.create_pull_request()
920 pull_request = pr_util.create_pull_request()
921 pull_request.source_ref = 'branch:origin:1234567890abcdef'
921 pull_request.source_ref = 'branch:origin:1234567890abcdef'
922 pull_request.target_ref = 'branch:target:abcdef1234567890'
922 pull_request.target_ref = 'branch:target:abcdef1234567890'
923 Session().add(pull_request)
923 Session().add(pull_request)
924 Session().commit()
924 Session().commit()
925
925
926 response = self.app.get(route_path(
926 response = self.app.get(route_path(
927 'pullrequest_show',
927 'pullrequest_show',
928 repo_name=pull_request.target_repo.scm_instance().name,
928 repo_name=pull_request.target_repo.scm_instance().name,
929 pull_request_id=pull_request.pull_request_id))
929 pull_request_id=pull_request.pull_request_id))
930 assert response.status_int == 200
930 assert response.status_int == 200
931 assert_response = AssertResponse(response)
931 assert_response = AssertResponse(response)
932
932
933 origin = assert_response.get_element('.pr-origininfo .tag')
933 origin = assert_response.get_element('.pr-origininfo .tag')
934 origin_children = origin.getchildren()
934 origin_children = origin.getchildren()
935 assert len(origin_children) == 1
935 assert len(origin_children) == 1
936 target = assert_response.get_element('.pr-targetinfo .tag')
936 target = assert_response.get_element('.pr-targetinfo .tag')
937 target_children = target.getchildren()
937 target_children = target.getchildren()
938 assert len(target_children) == 1
938 assert len(target_children) == 1
939
939
940 expected_origin_link = route_path(
940 expected_origin_link = route_path(
941 'repo_changelog',
941 'repo_changelog',
942 repo_name=pull_request.source_repo.scm_instance().name,
942 repo_name=pull_request.source_repo.scm_instance().name,
943 params=dict(branch='origin'))
943 params=dict(branch='origin'))
944 expected_target_link = route_path(
944 expected_target_link = route_path(
945 'repo_changelog',
945 'repo_changelog',
946 repo_name=pull_request.target_repo.scm_instance().name,
946 repo_name=pull_request.target_repo.scm_instance().name,
947 params=dict(branch='target'))
947 params=dict(branch='target'))
948 assert origin_children[0].attrib['href'] == expected_origin_link
948 assert origin_children[0].attrib['href'] == expected_origin_link
949 assert origin_children[0].text == 'branch: origin'
949 assert origin_children[0].text == 'branch: origin'
950 assert target_children[0].attrib['href'] == expected_target_link
950 assert target_children[0].attrib['href'] == expected_target_link
951 assert target_children[0].text == 'branch: target'
951 assert target_children[0].text == 'branch: target'
952
952
953 def test_bookmark_is_not_a_link(self, pr_util):
953 def test_bookmark_is_not_a_link(self, pr_util):
954 pull_request = pr_util.create_pull_request()
954 pull_request = pr_util.create_pull_request()
955 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
955 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
956 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
956 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
957 Session().add(pull_request)
957 Session().add(pull_request)
958 Session().commit()
958 Session().commit()
959
959
960 response = self.app.get(route_path(
960 response = self.app.get(route_path(
961 'pullrequest_show',
961 'pullrequest_show',
962 repo_name=pull_request.target_repo.scm_instance().name,
962 repo_name=pull_request.target_repo.scm_instance().name,
963 pull_request_id=pull_request.pull_request_id))
963 pull_request_id=pull_request.pull_request_id))
964 assert response.status_int == 200
964 assert response.status_int == 200
965 assert_response = AssertResponse(response)
965 assert_response = AssertResponse(response)
966
966
967 origin = assert_response.get_element('.pr-origininfo .tag')
967 origin = assert_response.get_element('.pr-origininfo .tag')
968 assert origin.text.strip() == 'bookmark: origin'
968 assert origin.text.strip() == 'bookmark: origin'
969 assert origin.getchildren() == []
969 assert origin.getchildren() == []
970
970
971 target = assert_response.get_element('.pr-targetinfo .tag')
971 target = assert_response.get_element('.pr-targetinfo .tag')
972 assert target.text.strip() == 'bookmark: target'
972 assert target.text.strip() == 'bookmark: target'
973 assert target.getchildren() == []
973 assert target.getchildren() == []
974
974
975 def test_tag_is_not_a_link(self, pr_util):
975 def test_tag_is_not_a_link(self, pr_util):
976 pull_request = pr_util.create_pull_request()
976 pull_request = pr_util.create_pull_request()
977 pull_request.source_ref = 'tag:origin:1234567890abcdef'
977 pull_request.source_ref = 'tag:origin:1234567890abcdef'
978 pull_request.target_ref = 'tag:target:abcdef1234567890'
978 pull_request.target_ref = 'tag:target:abcdef1234567890'
979 Session().add(pull_request)
979 Session().add(pull_request)
980 Session().commit()
980 Session().commit()
981
981
982 response = self.app.get(route_path(
982 response = self.app.get(route_path(
983 'pullrequest_show',
983 'pullrequest_show',
984 repo_name=pull_request.target_repo.scm_instance().name,
984 repo_name=pull_request.target_repo.scm_instance().name,
985 pull_request_id=pull_request.pull_request_id))
985 pull_request_id=pull_request.pull_request_id))
986 assert response.status_int == 200
986 assert response.status_int == 200
987 assert_response = AssertResponse(response)
987 assert_response = AssertResponse(response)
988
988
989 origin = assert_response.get_element('.pr-origininfo .tag')
989 origin = assert_response.get_element('.pr-origininfo .tag')
990 assert origin.text.strip() == 'tag: origin'
990 assert origin.text.strip() == 'tag: origin'
991 assert origin.getchildren() == []
991 assert origin.getchildren() == []
992
992
993 target = assert_response.get_element('.pr-targetinfo .tag')
993 target = assert_response.get_element('.pr-targetinfo .tag')
994 assert target.text.strip() == 'tag: target'
994 assert target.text.strip() == 'tag: target'
995 assert target.getchildren() == []
995 assert target.getchildren() == []
996
996
997 @pytest.mark.parametrize('mergeable', [True, False])
997 @pytest.mark.parametrize('mergeable', [True, False])
998 def test_shadow_repository_link(
998 def test_shadow_repository_link(
999 self, mergeable, pr_util, http_host_only_stub):
999 self, mergeable, pr_util, http_host_only_stub):
1000 """
1000 """
1001 Check that the pull request summary page displays a link to the shadow
1001 Check that the pull request summary page displays a link to the shadow
1002 repository if the pull request is mergeable. If it is not mergeable
1002 repository if the pull request is mergeable. If it is not mergeable
1003 the link should not be displayed.
1003 the link should not be displayed.
1004 """
1004 """
1005 pull_request = pr_util.create_pull_request(
1005 pull_request = pr_util.create_pull_request(
1006 mergeable=mergeable, enable_notifications=False)
1006 mergeable=mergeable, enable_notifications=False)
1007 target_repo = pull_request.target_repo.scm_instance()
1007 target_repo = pull_request.target_repo.scm_instance()
1008 pr_id = pull_request.pull_request_id
1008 pr_id = pull_request.pull_request_id
1009 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1009 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1010 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1010 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1011
1011
1012 response = self.app.get(route_path(
1012 response = self.app.get(route_path(
1013 'pullrequest_show',
1013 'pullrequest_show',
1014 repo_name=target_repo.name,
1014 repo_name=target_repo.name,
1015 pull_request_id=pr_id))
1015 pull_request_id=pr_id))
1016
1016
1017 assertr = AssertResponse(response)
1017 assertr = AssertResponse(response)
1018 if mergeable:
1018 if mergeable:
1019 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1019 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1020 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1020 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1021 else:
1021 else:
1022 assertr.no_element_exists('.pr-mergeinfo')
1022 assertr.no_element_exists('.pr-mergeinfo')
1023
1023
1024
1024
1025 @pytest.mark.usefixtures('app')
1025 @pytest.mark.usefixtures('app')
1026 @pytest.mark.backends("git", "hg")
1026 @pytest.mark.backends("git", "hg")
1027 class TestPullrequestsControllerDelete(object):
1027 class TestPullrequestsControllerDelete(object):
1028 def test_pull_request_delete_button_permissions_admin(
1028 def test_pull_request_delete_button_permissions_admin(
1029 self, autologin_user, user_admin, pr_util):
1029 self, autologin_user, user_admin, pr_util):
1030 pull_request = pr_util.create_pull_request(
1030 pull_request = pr_util.create_pull_request(
1031 author=user_admin.username, enable_notifications=False)
1031 author=user_admin.username, enable_notifications=False)
1032
1032
1033 response = self.app.get(route_path(
1033 response = self.app.get(route_path(
1034 'pullrequest_show',
1034 'pullrequest_show',
1035 repo_name=pull_request.target_repo.scm_instance().name,
1035 repo_name=pull_request.target_repo.scm_instance().name,
1036 pull_request_id=pull_request.pull_request_id))
1036 pull_request_id=pull_request.pull_request_id))
1037
1037
1038 response.mustcontain('id="delete_pullrequest"')
1038 response.mustcontain('id="delete_pullrequest"')
1039 response.mustcontain('Confirm to delete this pull request')
1039 response.mustcontain('Confirm to delete this pull request')
1040
1040
1041 def test_pull_request_delete_button_permissions_owner(
1041 def test_pull_request_delete_button_permissions_owner(
1042 self, autologin_regular_user, user_regular, pr_util):
1042 self, autologin_regular_user, user_regular, pr_util):
1043 pull_request = pr_util.create_pull_request(
1043 pull_request = pr_util.create_pull_request(
1044 author=user_regular.username, enable_notifications=False)
1044 author=user_regular.username, enable_notifications=False)
1045
1045
1046 response = self.app.get(route_path(
1046 response = self.app.get(route_path(
1047 'pullrequest_show',
1047 'pullrequest_show',
1048 repo_name=pull_request.target_repo.scm_instance().name,
1048 repo_name=pull_request.target_repo.scm_instance().name,
1049 pull_request_id=pull_request.pull_request_id))
1049 pull_request_id=pull_request.pull_request_id))
1050
1050
1051 response.mustcontain('id="delete_pullrequest"')
1051 response.mustcontain('id="delete_pullrequest"')
1052 response.mustcontain('Confirm to delete this pull request')
1052 response.mustcontain('Confirm to delete this pull request')
1053
1053
1054 def test_pull_request_delete_button_permissions_forbidden(
1054 def test_pull_request_delete_button_permissions_forbidden(
1055 self, autologin_regular_user, user_regular, user_admin, pr_util):
1055 self, autologin_regular_user, user_regular, user_admin, pr_util):
1056 pull_request = pr_util.create_pull_request(
1056 pull_request = pr_util.create_pull_request(
1057 author=user_admin.username, enable_notifications=False)
1057 author=user_admin.username, enable_notifications=False)
1058
1058
1059 response = self.app.get(route_path(
1059 response = self.app.get(route_path(
1060 'pullrequest_show',
1060 'pullrequest_show',
1061 repo_name=pull_request.target_repo.scm_instance().name,
1061 repo_name=pull_request.target_repo.scm_instance().name,
1062 pull_request_id=pull_request.pull_request_id))
1062 pull_request_id=pull_request.pull_request_id))
1063 response.mustcontain(no=['id="delete_pullrequest"'])
1063 response.mustcontain(no=['id="delete_pullrequest"'])
1064 response.mustcontain(no=['Confirm to delete this pull request'])
1064 response.mustcontain(no=['Confirm to delete this pull request'])
1065
1065
1066 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1066 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1067 self, autologin_regular_user, user_regular, user_admin, pr_util,
1067 self, autologin_regular_user, user_regular, user_admin, pr_util,
1068 user_util):
1068 user_util):
1069
1069
1070 pull_request = pr_util.create_pull_request(
1070 pull_request = pr_util.create_pull_request(
1071 author=user_admin.username, enable_notifications=False)
1071 author=user_admin.username, enable_notifications=False)
1072
1072
1073 user_util.grant_user_permission_to_repo(
1073 user_util.grant_user_permission_to_repo(
1074 pull_request.target_repo, user_regular,
1074 pull_request.target_repo, user_regular,
1075 'repository.write')
1075 'repository.write')
1076
1076
1077 response = self.app.get(route_path(
1077 response = self.app.get(route_path(
1078 'pullrequest_show',
1078 'pullrequest_show',
1079 repo_name=pull_request.target_repo.scm_instance().name,
1079 repo_name=pull_request.target_repo.scm_instance().name,
1080 pull_request_id=pull_request.pull_request_id))
1080 pull_request_id=pull_request.pull_request_id))
1081
1081
1082 response.mustcontain('id="open_edit_pullrequest"')
1082 response.mustcontain('id="open_edit_pullrequest"')
1083 response.mustcontain('id="delete_pullrequest"')
1083 response.mustcontain('id="delete_pullrequest"')
1084 response.mustcontain(no=['Confirm to delete this pull request'])
1084 response.mustcontain(no=['Confirm to delete this pull request'])
1085
1085
1086 def test_delete_comment_returns_404_if_comment_does_not_exist(
1086 def test_delete_comment_returns_404_if_comment_does_not_exist(
1087 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1087 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1088
1088
1089 pull_request = pr_util.create_pull_request(
1089 pull_request = pr_util.create_pull_request(
1090 author=user_admin.username, enable_notifications=False)
1090 author=user_admin.username, enable_notifications=False)
1091
1091
1092 self.app.post(
1092 self.app.post(
1093 route_path(
1093 route_path(
1094 'pullrequest_comment_delete',
1094 'pullrequest_comment_delete',
1095 repo_name=pull_request.target_repo.scm_instance().name,
1095 repo_name=pull_request.target_repo.scm_instance().name,
1096 pull_request_id=pull_request.pull_request_id,
1096 pull_request_id=pull_request.pull_request_id,
1097 comment_id=1024404),
1097 comment_id=1024404),
1098 extra_environ=xhr_header,
1098 extra_environ=xhr_header,
1099 params={'csrf_token': csrf_token},
1099 params={'csrf_token': csrf_token},
1100 status=404
1100 status=404
1101 )
1101 )
1102
1102
1103 def test_delete_comment(
1103 def test_delete_comment(
1104 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1104 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1105
1105
1106 pull_request = pr_util.create_pull_request(
1106 pull_request = pr_util.create_pull_request(
1107 author=user_admin.username, enable_notifications=False)
1107 author=user_admin.username, enable_notifications=False)
1108 comment = pr_util.create_comment()
1108 comment = pr_util.create_comment()
1109 comment_id = comment.comment_id
1109 comment_id = comment.comment_id
1110
1110
1111 response = self.app.post(
1111 response = self.app.post(
1112 route_path(
1112 route_path(
1113 'pullrequest_comment_delete',
1113 'pullrequest_comment_delete',
1114 repo_name=pull_request.target_repo.scm_instance().name,
1114 repo_name=pull_request.target_repo.scm_instance().name,
1115 pull_request_id=pull_request.pull_request_id,
1115 pull_request_id=pull_request.pull_request_id,
1116 comment_id=comment_id),
1116 comment_id=comment_id),
1117 extra_environ=xhr_header,
1117 extra_environ=xhr_header,
1118 params={'csrf_token': csrf_token},
1118 params={'csrf_token': csrf_token},
1119 status=200
1119 status=200
1120 )
1120 )
1121 assert response.body == 'true'
1121 assert response.body == 'true'
1122
1122
1123
1123
1124 def assert_pull_request_status(pull_request, expected_status):
1124 def assert_pull_request_status(pull_request, expected_status):
1125 status = ChangesetStatusModel().calculated_review_status(
1125 status = ChangesetStatusModel().calculated_review_status(
1126 pull_request=pull_request)
1126 pull_request=pull_request)
1127 assert status == expected_status
1127 assert status == expected_status
1128
1128
1129
1129
1130 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1130 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1131 @pytest.mark.usefixtures("autologin_user")
1131 @pytest.mark.usefixtures("autologin_user")
1132 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1132 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1133 response = app.get(
1133 response = app.get(
1134 route_path(route, repo_name=backend_svn.repo_name), status=404)
1134 route_path(route, repo_name=backend_svn.repo_name), status=404)
1135
1135
@@ -1,1192 +1,1196 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 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.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
44 from rhodecode.model.changeset_status import ChangesetStatusModel
44 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.comment import CommentsModel
45 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 ChangesetComment, ChangesetStatus, Repository)
47 ChangesetComment, ChangesetStatus, Repository)
48 from rhodecode.model.forms import PullRequestForm
48 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
50 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.scm import ScmModel
51 from rhodecode.model.scm import ScmModel
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class RepoPullRequestsView(RepoAppView, DataGridAppView):
56 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57
57
58 def load_default_context(self):
58 def load_default_context(self):
59 c = self._get_local_tmpl_context(include_app_defaults=True)
59 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
60 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
61 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 self._register_global_c(c)
62 self._register_global_c(c)
63 return c
63 return c
64
64
65 def _get_pull_requests_list(
65 def _get_pull_requests_list(
66 self, repo_name, source, filter_type, opened_by, statuses):
66 self, repo_name, source, filter_type, opened_by, statuses):
67
67
68 draw, start, limit = self._extract_chunk(self.request)
68 draw, start, limit = self._extract_chunk(self.request)
69 search_q, order_by, order_dir = self._extract_ordering(self.request)
69 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 _render = self.request.get_partial_renderer(
70 _render = self.request.get_partial_renderer(
71 'data_table/_dt_elements.mako')
71 'data_table/_dt_elements.mako')
72
72
73 # pagination
73 # pagination
74
74
75 if filter_type == 'awaiting_review':
75 if filter_type == 'awaiting_review':
76 pull_requests = PullRequestModel().get_awaiting_review(
76 pull_requests = PullRequestModel().get_awaiting_review(
77 repo_name, source=source, opened_by=opened_by,
77 repo_name, source=source, opened_by=opened_by,
78 statuses=statuses, offset=start, length=limit,
78 statuses=statuses, offset=start, length=limit,
79 order_by=order_by, order_dir=order_dir)
79 order_by=order_by, order_dir=order_dir)
80 pull_requests_total_count = PullRequestModel().count_awaiting_review(
80 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 repo_name, source=source, statuses=statuses,
81 repo_name, source=source, statuses=statuses,
82 opened_by=opened_by)
82 opened_by=opened_by)
83 elif filter_type == 'awaiting_my_review':
83 elif filter_type == 'awaiting_my_review':
84 pull_requests = PullRequestModel().get_awaiting_my_review(
84 pull_requests = PullRequestModel().get_awaiting_my_review(
85 repo_name, source=source, opened_by=opened_by,
85 repo_name, source=source, opened_by=opened_by,
86 user_id=self._rhodecode_user.user_id, statuses=statuses,
86 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 offset=start, length=limit, order_by=order_by,
87 offset=start, length=limit, order_by=order_by,
88 order_dir=order_dir)
88 order_dir=order_dir)
89 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
89 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 repo_name, source=source, user_id=self._rhodecode_user.user_id,
90 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 statuses=statuses, opened_by=opened_by)
91 statuses=statuses, opened_by=opened_by)
92 else:
92 else:
93 pull_requests = PullRequestModel().get_all(
93 pull_requests = PullRequestModel().get_all(
94 repo_name, source=source, opened_by=opened_by,
94 repo_name, source=source, opened_by=opened_by,
95 statuses=statuses, offset=start, length=limit,
95 statuses=statuses, offset=start, length=limit,
96 order_by=order_by, order_dir=order_dir)
96 order_by=order_by, order_dir=order_dir)
97 pull_requests_total_count = PullRequestModel().count_all(
97 pull_requests_total_count = PullRequestModel().count_all(
98 repo_name, source=source, statuses=statuses,
98 repo_name, source=source, statuses=statuses,
99 opened_by=opened_by)
99 opened_by=opened_by)
100
100
101 data = []
101 data = []
102 comments_model = CommentsModel()
102 comments_model = CommentsModel()
103 for pr in pull_requests:
103 for pr in pull_requests:
104 comments = comments_model.get_all_comments(
104 comments = comments_model.get_all_comments(
105 self.db_repo.repo_id, pull_request=pr)
105 self.db_repo.repo_id, pull_request=pr)
106
106
107 data.append({
107 data.append({
108 'name': _render('pullrequest_name',
108 'name': _render('pullrequest_name',
109 pr.pull_request_id, pr.target_repo.repo_name),
109 pr.pull_request_id, pr.target_repo.repo_name),
110 'name_raw': pr.pull_request_id,
110 'name_raw': pr.pull_request_id,
111 'status': _render('pullrequest_status',
111 'status': _render('pullrequest_status',
112 pr.calculated_review_status()),
112 pr.calculated_review_status()),
113 'title': _render(
113 'title': _render(
114 'pullrequest_title', pr.title, pr.description),
114 'pullrequest_title', pr.title, pr.description),
115 'description': h.escape(pr.description),
115 'description': h.escape(pr.description),
116 'updated_on': _render('pullrequest_updated_on',
116 'updated_on': _render('pullrequest_updated_on',
117 h.datetime_to_time(pr.updated_on)),
117 h.datetime_to_time(pr.updated_on)),
118 'updated_on_raw': h.datetime_to_time(pr.updated_on),
118 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'created_on': _render('pullrequest_updated_on',
119 'created_on': _render('pullrequest_updated_on',
120 h.datetime_to_time(pr.created_on)),
120 h.datetime_to_time(pr.created_on)),
121 'created_on_raw': h.datetime_to_time(pr.created_on),
121 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'author': _render('pullrequest_author',
122 'author': _render('pullrequest_author',
123 pr.author.full_contact, ),
123 pr.author.full_contact, ),
124 'author_raw': pr.author.full_name,
124 'author_raw': pr.author.full_name,
125 'comments': _render('pullrequest_comments', len(comments)),
125 'comments': _render('pullrequest_comments', len(comments)),
126 'comments_raw': len(comments),
126 'comments_raw': len(comments),
127 'closed': pr.is_closed(),
127 'closed': pr.is_closed(),
128 })
128 })
129
129
130 data = ({
130 data = ({
131 'draw': draw,
131 'draw': draw,
132 'data': data,
132 'data': data,
133 'recordsTotal': pull_requests_total_count,
133 'recordsTotal': pull_requests_total_count,
134 'recordsFiltered': pull_requests_total_count,
134 'recordsFiltered': pull_requests_total_count,
135 })
135 })
136 return data
136 return data
137
137
138 @LoginRequired()
138 @LoginRequired()
139 @HasRepoPermissionAnyDecorator(
139 @HasRepoPermissionAnyDecorator(
140 'repository.read', 'repository.write', 'repository.admin')
140 'repository.read', 'repository.write', 'repository.admin')
141 @view_config(
141 @view_config(
142 route_name='pullrequest_show_all', request_method='GET',
142 route_name='pullrequest_show_all', request_method='GET',
143 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
143 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 def pull_request_list(self):
144 def pull_request_list(self):
145 c = self.load_default_context()
145 c = self.load_default_context()
146
146
147 req_get = self.request.GET
147 req_get = self.request.GET
148 c.source = str2bool(req_get.get('source'))
148 c.source = str2bool(req_get.get('source'))
149 c.closed = str2bool(req_get.get('closed'))
149 c.closed = str2bool(req_get.get('closed'))
150 c.my = str2bool(req_get.get('my'))
150 c.my = str2bool(req_get.get('my'))
151 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
151 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
152 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153
153
154 c.active = 'open'
154 c.active = 'open'
155 if c.my:
155 if c.my:
156 c.active = 'my'
156 c.active = 'my'
157 if c.closed:
157 if c.closed:
158 c.active = 'closed'
158 c.active = 'closed'
159 if c.awaiting_review and not c.source:
159 if c.awaiting_review and not c.source:
160 c.active = 'awaiting'
160 c.active = 'awaiting'
161 if c.source and not c.awaiting_review:
161 if c.source and not c.awaiting_review:
162 c.active = 'source'
162 c.active = 'source'
163 if c.awaiting_my_review:
163 if c.awaiting_my_review:
164 c.active = 'awaiting_my'
164 c.active = 'awaiting_my'
165
165
166 return self._get_template_context(c)
166 return self._get_template_context(c)
167
167
168 @LoginRequired()
168 @LoginRequired()
169 @HasRepoPermissionAnyDecorator(
169 @HasRepoPermissionAnyDecorator(
170 'repository.read', 'repository.write', 'repository.admin')
170 'repository.read', 'repository.write', 'repository.admin')
171 @view_config(
171 @view_config(
172 route_name='pullrequest_show_all_data', request_method='GET',
172 route_name='pullrequest_show_all_data', request_method='GET',
173 renderer='json_ext', xhr=True)
173 renderer='json_ext', xhr=True)
174 def pull_request_list_data(self):
174 def pull_request_list_data(self):
175
175
176 # additional filters
176 # additional filters
177 req_get = self.request.GET
177 req_get = self.request.GET
178 source = str2bool(req_get.get('source'))
178 source = str2bool(req_get.get('source'))
179 closed = str2bool(req_get.get('closed'))
179 closed = str2bool(req_get.get('closed'))
180 my = str2bool(req_get.get('my'))
180 my = str2bool(req_get.get('my'))
181 awaiting_review = str2bool(req_get.get('awaiting_review'))
181 awaiting_review = str2bool(req_get.get('awaiting_review'))
182 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
182 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
183
183
184 filter_type = 'awaiting_review' if awaiting_review \
184 filter_type = 'awaiting_review' if awaiting_review \
185 else 'awaiting_my_review' if awaiting_my_review \
185 else 'awaiting_my_review' if awaiting_my_review \
186 else None
186 else None
187
187
188 opened_by = None
188 opened_by = None
189 if my:
189 if my:
190 opened_by = [self._rhodecode_user.user_id]
190 opened_by = [self._rhodecode_user.user_id]
191
191
192 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
192 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
193 if closed:
193 if closed:
194 statuses = [PullRequest.STATUS_CLOSED]
194 statuses = [PullRequest.STATUS_CLOSED]
195
195
196 data = self._get_pull_requests_list(
196 data = self._get_pull_requests_list(
197 repo_name=self.db_repo_name, source=source,
197 repo_name=self.db_repo_name, source=source,
198 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
198 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
199
199
200 return data
200 return data
201
201
202 def _get_pr_version(self, pull_request_id, version=None):
202 def _get_pr_version(self, pull_request_id, version=None):
203 at_version = None
203 at_version = None
204
204
205 if version and version == 'latest':
205 if version and version == 'latest':
206 pull_request_ver = PullRequest.get(pull_request_id)
206 pull_request_ver = PullRequest.get(pull_request_id)
207 pull_request_obj = pull_request_ver
207 pull_request_obj = pull_request_ver
208 _org_pull_request_obj = pull_request_obj
208 _org_pull_request_obj = pull_request_obj
209 at_version = 'latest'
209 at_version = 'latest'
210 elif version:
210 elif version:
211 pull_request_ver = PullRequestVersion.get_or_404(version)
211 pull_request_ver = PullRequestVersion.get_or_404(version)
212 pull_request_obj = pull_request_ver
212 pull_request_obj = pull_request_ver
213 _org_pull_request_obj = pull_request_ver.pull_request
213 _org_pull_request_obj = pull_request_ver.pull_request
214 at_version = pull_request_ver.pull_request_version_id
214 at_version = pull_request_ver.pull_request_version_id
215 else:
215 else:
216 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
216 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
217 pull_request_id)
217 pull_request_id)
218
218
219 pull_request_display_obj = PullRequest.get_pr_display_object(
219 pull_request_display_obj = PullRequest.get_pr_display_object(
220 pull_request_obj, _org_pull_request_obj)
220 pull_request_obj, _org_pull_request_obj)
221
221
222 return _org_pull_request_obj, pull_request_obj, \
222 return _org_pull_request_obj, pull_request_obj, \
223 pull_request_display_obj, at_version
223 pull_request_display_obj, at_version
224
224
225 def _get_diffset(self, source_repo_name, source_repo,
225 def _get_diffset(self, source_repo_name, source_repo,
226 source_ref_id, target_ref_id,
226 source_ref_id, target_ref_id,
227 target_commit, source_commit, diff_limit, fulldiff,
227 target_commit, source_commit, diff_limit, fulldiff,
228 file_limit, display_inline_comments):
228 file_limit, display_inline_comments):
229
229
230 vcs_diff = PullRequestModel().get_diff(
230 vcs_diff = PullRequestModel().get_diff(
231 source_repo, source_ref_id, target_ref_id)
231 source_repo, source_ref_id, target_ref_id)
232
232
233 diff_processor = diffs.DiffProcessor(
233 diff_processor = diffs.DiffProcessor(
234 vcs_diff, format='newdiff', diff_limit=diff_limit,
234 vcs_diff, format='newdiff', diff_limit=diff_limit,
235 file_limit=file_limit, show_full_diff=fulldiff)
235 file_limit=file_limit, show_full_diff=fulldiff)
236
236
237 _parsed = diff_processor.prepare()
237 _parsed = diff_processor.prepare()
238
238
239 def _node_getter(commit):
239 def _node_getter(commit):
240 def get_node(fname):
240 def get_node(fname):
241 try:
241 try:
242 return commit.get_node(fname)
242 return commit.get_node(fname)
243 except NodeDoesNotExistError:
243 except NodeDoesNotExistError:
244 return None
244 return None
245
245
246 return get_node
246 return get_node
247
247
248 diffset = codeblocks.DiffSet(
248 diffset = codeblocks.DiffSet(
249 repo_name=self.db_repo_name,
249 repo_name=self.db_repo_name,
250 source_repo_name=source_repo_name,
250 source_repo_name=source_repo_name,
251 source_node_getter=_node_getter(target_commit),
251 source_node_getter=_node_getter(target_commit),
252 target_node_getter=_node_getter(source_commit),
252 target_node_getter=_node_getter(source_commit),
253 comments=display_inline_comments
253 comments=display_inline_comments
254 )
254 )
255 diffset = diffset.render_patchset(
255 diffset = diffset.render_patchset(
256 _parsed, target_commit.raw_id, source_commit.raw_id)
256 _parsed, target_commit.raw_id, source_commit.raw_id)
257
257
258 return diffset
258 return diffset
259
259
260 @LoginRequired()
260 @LoginRequired()
261 @HasRepoPermissionAnyDecorator(
261 @HasRepoPermissionAnyDecorator(
262 'repository.read', 'repository.write', 'repository.admin')
262 'repository.read', 'repository.write', 'repository.admin')
263 @view_config(
263 @view_config(
264 route_name='pullrequest_show', request_method='GET',
264 route_name='pullrequest_show', request_method='GET',
265 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
265 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
266 def pull_request_show(self):
266 def pull_request_show(self):
267 pull_request_id = self.request.matchdict['pull_request_id']
267 pull_request_id = self.request.matchdict['pull_request_id']
268
268
269 c = self.load_default_context()
269 c = self.load_default_context()
270
270
271 version = self.request.GET.get('version')
271 version = self.request.GET.get('version')
272 from_version = self.request.GET.get('from_version') or version
272 from_version = self.request.GET.get('from_version') or version
273 merge_checks = self.request.GET.get('merge_checks')
273 merge_checks = self.request.GET.get('merge_checks')
274 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
274 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
275
275
276 (pull_request_latest,
276 (pull_request_latest,
277 pull_request_at_ver,
277 pull_request_at_ver,
278 pull_request_display_obj,
278 pull_request_display_obj,
279 at_version) = self._get_pr_version(
279 at_version) = self._get_pr_version(
280 pull_request_id, version=version)
280 pull_request_id, version=version)
281 pr_closed = pull_request_latest.is_closed()
281 pr_closed = pull_request_latest.is_closed()
282
282
283 if pr_closed and (version or from_version):
283 if pr_closed and (version or from_version):
284 # not allow to browse versions
284 # not allow to browse versions
285 raise HTTPFound(h.route_path(
285 raise HTTPFound(h.route_path(
286 'pullrequest_show', repo_name=self.db_repo_name,
286 'pullrequest_show', repo_name=self.db_repo_name,
287 pull_request_id=pull_request_id))
287 pull_request_id=pull_request_id))
288
288
289 versions = pull_request_display_obj.versions()
289 versions = pull_request_display_obj.versions()
290
290
291 c.at_version = at_version
291 c.at_version = at_version
292 c.at_version_num = (at_version
292 c.at_version_num = (at_version
293 if at_version and at_version != 'latest'
293 if at_version and at_version != 'latest'
294 else None)
294 else None)
295 c.at_version_pos = ChangesetComment.get_index_from_version(
295 c.at_version_pos = ChangesetComment.get_index_from_version(
296 c.at_version_num, versions)
296 c.at_version_num, versions)
297
297
298 (prev_pull_request_latest,
298 (prev_pull_request_latest,
299 prev_pull_request_at_ver,
299 prev_pull_request_at_ver,
300 prev_pull_request_display_obj,
300 prev_pull_request_display_obj,
301 prev_at_version) = self._get_pr_version(
301 prev_at_version) = self._get_pr_version(
302 pull_request_id, version=from_version)
302 pull_request_id, version=from_version)
303
303
304 c.from_version = prev_at_version
304 c.from_version = prev_at_version
305 c.from_version_num = (prev_at_version
305 c.from_version_num = (prev_at_version
306 if prev_at_version and prev_at_version != 'latest'
306 if prev_at_version and prev_at_version != 'latest'
307 else None)
307 else None)
308 c.from_version_pos = ChangesetComment.get_index_from_version(
308 c.from_version_pos = ChangesetComment.get_index_from_version(
309 c.from_version_num, versions)
309 c.from_version_num, versions)
310
310
311 # define if we're in COMPARE mode or VIEW at version mode
311 # define if we're in COMPARE mode or VIEW at version mode
312 compare = at_version != prev_at_version
312 compare = at_version != prev_at_version
313
313
314 # pull_requests repo_name we opened it against
314 # pull_requests repo_name we opened it against
315 # ie. target_repo must match
315 # ie. target_repo must match
316 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
316 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
317 raise HTTPNotFound()
317 raise HTTPNotFound()
318
318
319 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
319 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
320 pull_request_at_ver)
320 pull_request_at_ver)
321
321
322 c.pull_request = pull_request_display_obj
322 c.pull_request = pull_request_display_obj
323 c.pull_request_latest = pull_request_latest
323 c.pull_request_latest = pull_request_latest
324
324
325 if compare or (at_version and not at_version == 'latest'):
325 if compare or (at_version and not at_version == 'latest'):
326 c.allowed_to_change_status = False
326 c.allowed_to_change_status = False
327 c.allowed_to_update = False
327 c.allowed_to_update = False
328 c.allowed_to_merge = False
328 c.allowed_to_merge = False
329 c.allowed_to_delete = False
329 c.allowed_to_delete = False
330 c.allowed_to_comment = False
330 c.allowed_to_comment = False
331 c.allowed_to_close = False
331 c.allowed_to_close = False
332 else:
332 else:
333 can_change_status = PullRequestModel().check_user_change_status(
333 can_change_status = PullRequestModel().check_user_change_status(
334 pull_request_at_ver, self._rhodecode_user)
334 pull_request_at_ver, self._rhodecode_user)
335 c.allowed_to_change_status = can_change_status and not pr_closed
335 c.allowed_to_change_status = can_change_status and not pr_closed
336
336
337 c.allowed_to_update = PullRequestModel().check_user_update(
337 c.allowed_to_update = PullRequestModel().check_user_update(
338 pull_request_latest, self._rhodecode_user) and not pr_closed
338 pull_request_latest, self._rhodecode_user) and not pr_closed
339 c.allowed_to_merge = PullRequestModel().check_user_merge(
339 c.allowed_to_merge = PullRequestModel().check_user_merge(
340 pull_request_latest, self._rhodecode_user) and not pr_closed
340 pull_request_latest, self._rhodecode_user) and not pr_closed
341 c.allowed_to_delete = PullRequestModel().check_user_delete(
341 c.allowed_to_delete = PullRequestModel().check_user_delete(
342 pull_request_latest, self._rhodecode_user) and not pr_closed
342 pull_request_latest, self._rhodecode_user) and not pr_closed
343 c.allowed_to_comment = not pr_closed
343 c.allowed_to_comment = not pr_closed
344 c.allowed_to_close = c.allowed_to_merge and not pr_closed
344 c.allowed_to_close = c.allowed_to_merge and not pr_closed
345
345
346 c.forbid_adding_reviewers = False
346 c.forbid_adding_reviewers = False
347 c.forbid_author_to_review = False
347 c.forbid_author_to_review = False
348 c.forbid_commit_author_to_review = False
348 c.forbid_commit_author_to_review = False
349
349
350 if pull_request_latest.reviewer_data and \
350 if pull_request_latest.reviewer_data and \
351 'rules' in pull_request_latest.reviewer_data:
351 'rules' in pull_request_latest.reviewer_data:
352 rules = pull_request_latest.reviewer_data['rules'] or {}
352 rules = pull_request_latest.reviewer_data['rules'] or {}
353 try:
353 try:
354 c.forbid_adding_reviewers = rules.get(
354 c.forbid_adding_reviewers = rules.get(
355 'forbid_adding_reviewers')
355 'forbid_adding_reviewers')
356 c.forbid_author_to_review = rules.get(
356 c.forbid_author_to_review = rules.get(
357 'forbid_author_to_review')
357 'forbid_author_to_review')
358 c.forbid_commit_author_to_review = rules.get(
358 c.forbid_commit_author_to_review = rules.get(
359 'forbid_commit_author_to_review')
359 'forbid_commit_author_to_review')
360 except Exception:
360 except Exception:
361 pass
361 pass
362
362
363 # check merge capabilities
363 # check merge capabilities
364 _merge_check = MergeCheck.validate(
364 _merge_check = MergeCheck.validate(
365 pull_request_latest, user=self._rhodecode_user)
365 pull_request_latest, user=self._rhodecode_user,
366 translator=self.request.translate)
366 c.pr_merge_errors = _merge_check.error_details
367 c.pr_merge_errors = _merge_check.error_details
367 c.pr_merge_possible = not _merge_check.failed
368 c.pr_merge_possible = not _merge_check.failed
368 c.pr_merge_message = _merge_check.merge_msg
369 c.pr_merge_message = _merge_check.merge_msg
369
370
370 c.pr_merge_info = MergeCheck.get_merge_conditions(pull_request_latest)
371 c.pr_merge_info = MergeCheck.get_merge_conditions(
372 pull_request_latest, translator=self.request.translate)
371
373
372 c.pull_request_review_status = _merge_check.review_status
374 c.pull_request_review_status = _merge_check.review_status
373 if merge_checks:
375 if merge_checks:
374 self.request.override_renderer = \
376 self.request.override_renderer = \
375 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
377 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
376 return self._get_template_context(c)
378 return self._get_template_context(c)
377
379
378 comments_model = CommentsModel()
380 comments_model = CommentsModel()
379
381
380 # reviewers and statuses
382 # reviewers and statuses
381 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
383 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
382 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
384 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
383
385
384 # GENERAL COMMENTS with versions #
386 # GENERAL COMMENTS with versions #
385 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
387 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
386 q = q.order_by(ChangesetComment.comment_id.asc())
388 q = q.order_by(ChangesetComment.comment_id.asc())
387 general_comments = q
389 general_comments = q
388
390
389 # pick comments we want to render at current version
391 # pick comments we want to render at current version
390 c.comment_versions = comments_model.aggregate_comments(
392 c.comment_versions = comments_model.aggregate_comments(
391 general_comments, versions, c.at_version_num)
393 general_comments, versions, c.at_version_num)
392 c.comments = c.comment_versions[c.at_version_num]['until']
394 c.comments = c.comment_versions[c.at_version_num]['until']
393
395
394 # INLINE COMMENTS with versions #
396 # INLINE COMMENTS with versions #
395 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
397 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
396 q = q.order_by(ChangesetComment.comment_id.asc())
398 q = q.order_by(ChangesetComment.comment_id.asc())
397 inline_comments = q
399 inline_comments = q
398
400
399 c.inline_versions = comments_model.aggregate_comments(
401 c.inline_versions = comments_model.aggregate_comments(
400 inline_comments, versions, c.at_version_num, inline=True)
402 inline_comments, versions, c.at_version_num, inline=True)
401
403
402 # inject latest version
404 # inject latest version
403 latest_ver = PullRequest.get_pr_display_object(
405 latest_ver = PullRequest.get_pr_display_object(
404 pull_request_latest, pull_request_latest)
406 pull_request_latest, pull_request_latest)
405
407
406 c.versions = versions + [latest_ver]
408 c.versions = versions + [latest_ver]
407
409
408 # if we use version, then do not show later comments
410 # if we use version, then do not show later comments
409 # than current version
411 # than current version
410 display_inline_comments = collections.defaultdict(
412 display_inline_comments = collections.defaultdict(
411 lambda: collections.defaultdict(list))
413 lambda: collections.defaultdict(list))
412 for co in inline_comments:
414 for co in inline_comments:
413 if c.at_version_num:
415 if c.at_version_num:
414 # pick comments that are at least UPTO given version, so we
416 # pick comments that are at least UPTO given version, so we
415 # don't render comments for higher version
417 # don't render comments for higher version
416 should_render = co.pull_request_version_id and \
418 should_render = co.pull_request_version_id and \
417 co.pull_request_version_id <= c.at_version_num
419 co.pull_request_version_id <= c.at_version_num
418 else:
420 else:
419 # showing all, for 'latest'
421 # showing all, for 'latest'
420 should_render = True
422 should_render = True
421
423
422 if should_render:
424 if should_render:
423 display_inline_comments[co.f_path][co.line_no].append(co)
425 display_inline_comments[co.f_path][co.line_no].append(co)
424
426
425 # load diff data into template context, if we use compare mode then
427 # load diff data into template context, if we use compare mode then
426 # diff is calculated based on changes between versions of PR
428 # diff is calculated based on changes between versions of PR
427
429
428 source_repo = pull_request_at_ver.source_repo
430 source_repo = pull_request_at_ver.source_repo
429 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
431 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
430
432
431 target_repo = pull_request_at_ver.target_repo
433 target_repo = pull_request_at_ver.target_repo
432 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
434 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
433
435
434 if compare:
436 if compare:
435 # in compare switch the diff base to latest commit from prev version
437 # in compare switch the diff base to latest commit from prev version
436 target_ref_id = prev_pull_request_display_obj.revisions[0]
438 target_ref_id = prev_pull_request_display_obj.revisions[0]
437
439
438 # despite opening commits for bookmarks/branches/tags, we always
440 # despite opening commits for bookmarks/branches/tags, we always
439 # convert this to rev to prevent changes after bookmark or branch change
441 # convert this to rev to prevent changes after bookmark or branch change
440 c.source_ref_type = 'rev'
442 c.source_ref_type = 'rev'
441 c.source_ref = source_ref_id
443 c.source_ref = source_ref_id
442
444
443 c.target_ref_type = 'rev'
445 c.target_ref_type = 'rev'
444 c.target_ref = target_ref_id
446 c.target_ref = target_ref_id
445
447
446 c.source_repo = source_repo
448 c.source_repo = source_repo
447 c.target_repo = target_repo
449 c.target_repo = target_repo
448
450
449 c.commit_ranges = []
451 c.commit_ranges = []
450 source_commit = EmptyCommit()
452 source_commit = EmptyCommit()
451 target_commit = EmptyCommit()
453 target_commit = EmptyCommit()
452 c.missing_requirements = False
454 c.missing_requirements = False
453
455
454 source_scm = source_repo.scm_instance()
456 source_scm = source_repo.scm_instance()
455 target_scm = target_repo.scm_instance()
457 target_scm = target_repo.scm_instance()
456
458
457 # try first shadow repo, fallback to regular repo
459 # try first shadow repo, fallback to regular repo
458 try:
460 try:
459 commits_source_repo = pull_request_latest.get_shadow_repo()
461 commits_source_repo = pull_request_latest.get_shadow_repo()
460 except Exception:
462 except Exception:
461 log.debug('Failed to get shadow repo', exc_info=True)
463 log.debug('Failed to get shadow repo', exc_info=True)
462 commits_source_repo = source_scm
464 commits_source_repo = source_scm
463
465
464 c.commits_source_repo = commits_source_repo
466 c.commits_source_repo = commits_source_repo
465 commit_cache = {}
467 commit_cache = {}
466 try:
468 try:
467 pre_load = ["author", "branch", "date", "message"]
469 pre_load = ["author", "branch", "date", "message"]
468 show_revs = pull_request_at_ver.revisions
470 show_revs = pull_request_at_ver.revisions
469 for rev in show_revs:
471 for rev in show_revs:
470 comm = commits_source_repo.get_commit(
472 comm = commits_source_repo.get_commit(
471 commit_id=rev, pre_load=pre_load)
473 commit_id=rev, pre_load=pre_load)
472 c.commit_ranges.append(comm)
474 c.commit_ranges.append(comm)
473 commit_cache[comm.raw_id] = comm
475 commit_cache[comm.raw_id] = comm
474
476
475 # Order here matters, we first need to get target, and then
477 # Order here matters, we first need to get target, and then
476 # the source
478 # the source
477 target_commit = commits_source_repo.get_commit(
479 target_commit = commits_source_repo.get_commit(
478 commit_id=safe_str(target_ref_id))
480 commit_id=safe_str(target_ref_id))
479
481
480 source_commit = commits_source_repo.get_commit(
482 source_commit = commits_source_repo.get_commit(
481 commit_id=safe_str(source_ref_id))
483 commit_id=safe_str(source_ref_id))
482
484
483 except CommitDoesNotExistError:
485 except CommitDoesNotExistError:
484 log.warning(
486 log.warning(
485 'Failed to get commit from `{}` repo'.format(
487 'Failed to get commit from `{}` repo'.format(
486 commits_source_repo), exc_info=True)
488 commits_source_repo), exc_info=True)
487 except RepositoryRequirementError:
489 except RepositoryRequirementError:
488 log.warning(
490 log.warning(
489 'Failed to get all required data from repo', exc_info=True)
491 'Failed to get all required data from repo', exc_info=True)
490 c.missing_requirements = True
492 c.missing_requirements = True
491
493
492 c.ancestor = None # set it to None, to hide it from PR view
494 c.ancestor = None # set it to None, to hide it from PR view
493
495
494 try:
496 try:
495 ancestor_id = source_scm.get_common_ancestor(
497 ancestor_id = source_scm.get_common_ancestor(
496 source_commit.raw_id, target_commit.raw_id, target_scm)
498 source_commit.raw_id, target_commit.raw_id, target_scm)
497 c.ancestor_commit = source_scm.get_commit(ancestor_id)
499 c.ancestor_commit = source_scm.get_commit(ancestor_id)
498 except Exception:
500 except Exception:
499 c.ancestor_commit = None
501 c.ancestor_commit = None
500
502
501 c.statuses = source_repo.statuses(
503 c.statuses = source_repo.statuses(
502 [x.raw_id for x in c.commit_ranges])
504 [x.raw_id for x in c.commit_ranges])
503
505
504 # auto collapse if we have more than limit
506 # auto collapse if we have more than limit
505 collapse_limit = diffs.DiffProcessor._collapse_commits_over
507 collapse_limit = diffs.DiffProcessor._collapse_commits_over
506 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
508 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
507 c.compare_mode = compare
509 c.compare_mode = compare
508
510
509 # diff_limit is the old behavior, will cut off the whole diff
511 # diff_limit is the old behavior, will cut off the whole diff
510 # if the limit is applied otherwise will just hide the
512 # if the limit is applied otherwise will just hide the
511 # big files from the front-end
513 # big files from the front-end
512 diff_limit = c.visual.cut_off_limit_diff
514 diff_limit = c.visual.cut_off_limit_diff
513 file_limit = c.visual.cut_off_limit_file
515 file_limit = c.visual.cut_off_limit_file
514
516
515 c.missing_commits = False
517 c.missing_commits = False
516 if (c.missing_requirements
518 if (c.missing_requirements
517 or isinstance(source_commit, EmptyCommit)
519 or isinstance(source_commit, EmptyCommit)
518 or source_commit == target_commit):
520 or source_commit == target_commit):
519
521
520 c.missing_commits = True
522 c.missing_commits = True
521 else:
523 else:
522
524
523 c.diffset = self._get_diffset(
525 c.diffset = self._get_diffset(
524 c.source_repo.repo_name, commits_source_repo,
526 c.source_repo.repo_name, commits_source_repo,
525 source_ref_id, target_ref_id,
527 source_ref_id, target_ref_id,
526 target_commit, source_commit,
528 target_commit, source_commit,
527 diff_limit, c.fulldiff, file_limit, display_inline_comments)
529 diff_limit, c.fulldiff, file_limit, display_inline_comments)
528
530
529 c.limited_diff = c.diffset.limited_diff
531 c.limited_diff = c.diffset.limited_diff
530
532
531 # calculate removed files that are bound to comments
533 # calculate removed files that are bound to comments
532 comment_deleted_files = [
534 comment_deleted_files = [
533 fname for fname in display_inline_comments
535 fname for fname in display_inline_comments
534 if fname not in c.diffset.file_stats]
536 if fname not in c.diffset.file_stats]
535
537
536 c.deleted_files_comments = collections.defaultdict(dict)
538 c.deleted_files_comments = collections.defaultdict(dict)
537 for fname, per_line_comments in display_inline_comments.items():
539 for fname, per_line_comments in display_inline_comments.items():
538 if fname in comment_deleted_files:
540 if fname in comment_deleted_files:
539 c.deleted_files_comments[fname]['stats'] = 0
541 c.deleted_files_comments[fname]['stats'] = 0
540 c.deleted_files_comments[fname]['comments'] = list()
542 c.deleted_files_comments[fname]['comments'] = list()
541 for lno, comments in per_line_comments.items():
543 for lno, comments in per_line_comments.items():
542 c.deleted_files_comments[fname]['comments'].extend(
544 c.deleted_files_comments[fname]['comments'].extend(
543 comments)
545 comments)
544
546
545 # 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
546 # compare view and others uses different notation, and
548 # compare view and others uses different notation, and
547 # compare_commits.mako renders links based on the target_repo.
549 # compare_commits.mako renders links based on the target_repo.
548 # 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
549 c.target_repo = c.source_repo
551 c.target_repo = c.source_repo
550
552
551 c.commit_statuses = ChangesetStatus.STATUSES
553 c.commit_statuses = ChangesetStatus.STATUSES
552
554
553 c.show_version_changes = not pr_closed
555 c.show_version_changes = not pr_closed
554 if c.show_version_changes:
556 if c.show_version_changes:
555 cur_obj = pull_request_at_ver
557 cur_obj = pull_request_at_ver
556 prev_obj = prev_pull_request_at_ver
558 prev_obj = prev_pull_request_at_ver
557
559
558 old_commit_ids = prev_obj.revisions
560 old_commit_ids = prev_obj.revisions
559 new_commit_ids = cur_obj.revisions
561 new_commit_ids = cur_obj.revisions
560 commit_changes = PullRequestModel()._calculate_commit_id_changes(
562 commit_changes = PullRequestModel()._calculate_commit_id_changes(
561 old_commit_ids, new_commit_ids)
563 old_commit_ids, new_commit_ids)
562 c.commit_changes_summary = commit_changes
564 c.commit_changes_summary = commit_changes
563
565
564 # calculate the diff for commits between versions
566 # calculate the diff for commits between versions
565 c.commit_changes = []
567 c.commit_changes = []
566 mark = lambda cs, fw: list(
568 mark = lambda cs, fw: list(
567 h.itertools.izip_longest([], cs, fillvalue=fw))
569 h.itertools.izip_longest([], cs, fillvalue=fw))
568 for c_type, raw_id in mark(commit_changes.added, 'a') \
570 for c_type, raw_id in mark(commit_changes.added, 'a') \
569 + mark(commit_changes.removed, 'r') \
571 + mark(commit_changes.removed, 'r') \
570 + mark(commit_changes.common, 'c'):
572 + mark(commit_changes.common, 'c'):
571
573
572 if raw_id in commit_cache:
574 if raw_id in commit_cache:
573 commit = commit_cache[raw_id]
575 commit = commit_cache[raw_id]
574 else:
576 else:
575 try:
577 try:
576 commit = commits_source_repo.get_commit(raw_id)
578 commit = commits_source_repo.get_commit(raw_id)
577 except CommitDoesNotExistError:
579 except CommitDoesNotExistError:
578 # in case we fail extracting still use "dummy" commit
580 # in case we fail extracting still use "dummy" commit
579 # for display in commit diff
581 # for display in commit diff
580 commit = h.AttributeDict(
582 commit = h.AttributeDict(
581 {'raw_id': raw_id,
583 {'raw_id': raw_id,
582 'message': 'EMPTY or MISSING COMMIT'})
584 'message': 'EMPTY or MISSING COMMIT'})
583 c.commit_changes.append([c_type, commit])
585 c.commit_changes.append([c_type, commit])
584
586
585 # current user review statuses for each version
587 # current user review statuses for each version
586 c.review_versions = {}
588 c.review_versions = {}
587 if self._rhodecode_user.user_id in allowed_reviewers:
589 if self._rhodecode_user.user_id in allowed_reviewers:
588 for co in general_comments:
590 for co in general_comments:
589 if co.author.user_id == self._rhodecode_user.user_id:
591 if co.author.user_id == self._rhodecode_user.user_id:
590 # each comment has a status change
592 # each comment has a status change
591 status = co.status_change
593 status = co.status_change
592 if status:
594 if status:
593 _ver_pr = status[0].comment.pull_request_version_id
595 _ver_pr = status[0].comment.pull_request_version_id
594 c.review_versions[_ver_pr] = status[0]
596 c.review_versions[_ver_pr] = status[0]
595
597
596 return self._get_template_context(c)
598 return self._get_template_context(c)
597
599
598 def assure_not_empty_repo(self):
600 def assure_not_empty_repo(self):
599 _ = self.request.translate
601 _ = self.request.translate
600
602
601 try:
603 try:
602 self.db_repo.scm_instance().get_commit()
604 self.db_repo.scm_instance().get_commit()
603 except EmptyRepositoryError:
605 except EmptyRepositoryError:
604 h.flash(h.literal(_('There are no commits yet')),
606 h.flash(h.literal(_('There are no commits yet')),
605 category='warning')
607 category='warning')
606 raise HTTPFound(
608 raise HTTPFound(
607 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
609 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
608
610
609 @LoginRequired()
611 @LoginRequired()
610 @NotAnonymous()
612 @NotAnonymous()
611 @HasRepoPermissionAnyDecorator(
613 @HasRepoPermissionAnyDecorator(
612 'repository.read', 'repository.write', 'repository.admin')
614 'repository.read', 'repository.write', 'repository.admin')
613 @view_config(
615 @view_config(
614 route_name='pullrequest_new', request_method='GET',
616 route_name='pullrequest_new', request_method='GET',
615 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
617 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
616 def pull_request_new(self):
618 def pull_request_new(self):
617 _ = self.request.translate
619 _ = self.request.translate
618 c = self.load_default_context()
620 c = self.load_default_context()
619
621
620 self.assure_not_empty_repo()
622 self.assure_not_empty_repo()
621 source_repo = self.db_repo
623 source_repo = self.db_repo
622
624
623 commit_id = self.request.GET.get('commit')
625 commit_id = self.request.GET.get('commit')
624 branch_ref = self.request.GET.get('branch')
626 branch_ref = self.request.GET.get('branch')
625 bookmark_ref = self.request.GET.get('bookmark')
627 bookmark_ref = self.request.GET.get('bookmark')
626
628
627 try:
629 try:
628 source_repo_data = PullRequestModel().generate_repo_data(
630 source_repo_data = PullRequestModel().generate_repo_data(
629 source_repo, commit_id=commit_id,
631 source_repo, commit_id=commit_id,
630 branch=branch_ref, bookmark=bookmark_ref)
632 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
631 except CommitDoesNotExistError as e:
633 except CommitDoesNotExistError as e:
632 log.exception(e)
634 log.exception(e)
633 h.flash(_('Commit does not exist'), 'error')
635 h.flash(_('Commit does not exist'), 'error')
634 raise HTTPFound(
636 raise HTTPFound(
635 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
637 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
636
638
637 default_target_repo = source_repo
639 default_target_repo = source_repo
638
640
639 if source_repo.parent:
641 if source_repo.parent:
640 parent_vcs_obj = source_repo.parent.scm_instance()
642 parent_vcs_obj = source_repo.parent.scm_instance()
641 if parent_vcs_obj and not parent_vcs_obj.is_empty():
643 if parent_vcs_obj and not parent_vcs_obj.is_empty():
642 # change default if we have a parent repo
644 # change default if we have a parent repo
643 default_target_repo = source_repo.parent
645 default_target_repo = source_repo.parent
644
646
645 target_repo_data = PullRequestModel().generate_repo_data(
647 target_repo_data = PullRequestModel().generate_repo_data(
646 default_target_repo)
648 default_target_repo, translator=self.request.translate)
647
649
648 selected_source_ref = source_repo_data['refs']['selected_ref']
650 selected_source_ref = source_repo_data['refs']['selected_ref']
649
651
650 title_source_ref = selected_source_ref.split(':', 2)[1]
652 title_source_ref = selected_source_ref.split(':', 2)[1]
651 c.default_title = PullRequestModel().generate_pullrequest_title(
653 c.default_title = PullRequestModel().generate_pullrequest_title(
652 source=source_repo.repo_name,
654 source=source_repo.repo_name,
653 source_ref=title_source_ref,
655 source_ref=title_source_ref,
654 target=default_target_repo.repo_name
656 target=default_target_repo.repo_name
655 )
657 )
656
658
657 c.default_repo_data = {
659 c.default_repo_data = {
658 'source_repo_name': source_repo.repo_name,
660 'source_repo_name': source_repo.repo_name,
659 'source_refs_json': json.dumps(source_repo_data),
661 'source_refs_json': json.dumps(source_repo_data),
660 'target_repo_name': default_target_repo.repo_name,
662 'target_repo_name': default_target_repo.repo_name,
661 'target_refs_json': json.dumps(target_repo_data),
663 'target_refs_json': json.dumps(target_repo_data),
662 }
664 }
663 c.default_source_ref = selected_source_ref
665 c.default_source_ref = selected_source_ref
664
666
665 return self._get_template_context(c)
667 return self._get_template_context(c)
666
668
667 @LoginRequired()
669 @LoginRequired()
668 @NotAnonymous()
670 @NotAnonymous()
669 @HasRepoPermissionAnyDecorator(
671 @HasRepoPermissionAnyDecorator(
670 'repository.read', 'repository.write', 'repository.admin')
672 'repository.read', 'repository.write', 'repository.admin')
671 @view_config(
673 @view_config(
672 route_name='pullrequest_repo_refs', request_method='GET',
674 route_name='pullrequest_repo_refs', request_method='GET',
673 renderer='json_ext', xhr=True)
675 renderer='json_ext', xhr=True)
674 def pull_request_repo_refs(self):
676 def pull_request_repo_refs(self):
675 target_repo_name = self.request.matchdict['target_repo_name']
677 target_repo_name = self.request.matchdict['target_repo_name']
676 repo = Repository.get_by_repo_name(target_repo_name)
678 repo = Repository.get_by_repo_name(target_repo_name)
677 if not repo:
679 if not repo:
678 raise HTTPNotFound()
680 raise HTTPNotFound()
679 return PullRequestModel().generate_repo_data(repo)
681 return PullRequestModel().generate_repo_data(repo, translator=self.request.translate)
680
682
681 @LoginRequired()
683 @LoginRequired()
682 @NotAnonymous()
684 @NotAnonymous()
683 @HasRepoPermissionAnyDecorator(
685 @HasRepoPermissionAnyDecorator(
684 'repository.read', 'repository.write', 'repository.admin')
686 'repository.read', 'repository.write', 'repository.admin')
685 @view_config(
687 @view_config(
686 route_name='pullrequest_repo_destinations', request_method='GET',
688 route_name='pullrequest_repo_destinations', request_method='GET',
687 renderer='json_ext', xhr=True)
689 renderer='json_ext', xhr=True)
688 def pull_request_repo_destinations(self):
690 def pull_request_repo_destinations(self):
689 _ = self.request.translate
691 _ = self.request.translate
690 filter_query = self.request.GET.get('query')
692 filter_query = self.request.GET.get('query')
691
693
692 query = Repository.query() \
694 query = Repository.query() \
693 .order_by(func.length(Repository.repo_name)) \
695 .order_by(func.length(Repository.repo_name)) \
694 .filter(
696 .filter(
695 or_(Repository.repo_name == self.db_repo.repo_name,
697 or_(Repository.repo_name == self.db_repo.repo_name,
696 Repository.fork_id == self.db_repo.repo_id))
698 Repository.fork_id == self.db_repo.repo_id))
697
699
698 if filter_query:
700 if filter_query:
699 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
701 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
700 query = query.filter(
702 query = query.filter(
701 Repository.repo_name.ilike(ilike_expression))
703 Repository.repo_name.ilike(ilike_expression))
702
704
703 add_parent = False
705 add_parent = False
704 if self.db_repo.parent:
706 if self.db_repo.parent:
705 if filter_query in self.db_repo.parent.repo_name:
707 if filter_query in self.db_repo.parent.repo_name:
706 parent_vcs_obj = self.db_repo.parent.scm_instance()
708 parent_vcs_obj = self.db_repo.parent.scm_instance()
707 if parent_vcs_obj and not parent_vcs_obj.is_empty():
709 if parent_vcs_obj and not parent_vcs_obj.is_empty():
708 add_parent = True
710 add_parent = True
709
711
710 limit = 20 - 1 if add_parent else 20
712 limit = 20 - 1 if add_parent else 20
711 all_repos = query.limit(limit).all()
713 all_repos = query.limit(limit).all()
712 if add_parent:
714 if add_parent:
713 all_repos += [self.db_repo.parent]
715 all_repos += [self.db_repo.parent]
714
716
715 repos = []
717 repos = []
716 for obj in ScmModel().get_repos(all_repos):
718 for obj in ScmModel().get_repos(all_repos):
717 repos.append({
719 repos.append({
718 'id': obj['name'],
720 'id': obj['name'],
719 'text': obj['name'],
721 'text': obj['name'],
720 'type': 'repo',
722 'type': 'repo',
721 'obj': obj['dbrepo']
723 'obj': obj['dbrepo']
722 })
724 })
723
725
724 data = {
726 data = {
725 'more': False,
727 'more': False,
726 'results': [{
728 'results': [{
727 'text': _('Repositories'),
729 'text': _('Repositories'),
728 'children': repos
730 'children': repos
729 }] if repos else []
731 }] if repos else []
730 }
732 }
731 return data
733 return data
732
734
733 @LoginRequired()
735 @LoginRequired()
734 @NotAnonymous()
736 @NotAnonymous()
735 @HasRepoPermissionAnyDecorator(
737 @HasRepoPermissionAnyDecorator(
736 'repository.read', 'repository.write', 'repository.admin')
738 'repository.read', 'repository.write', 'repository.admin')
737 @CSRFRequired()
739 @CSRFRequired()
738 @view_config(
740 @view_config(
739 route_name='pullrequest_create', request_method='POST',
741 route_name='pullrequest_create', request_method='POST',
740 renderer=None)
742 renderer=None)
741 def pull_request_create(self):
743 def pull_request_create(self):
742 _ = self.request.translate
744 _ = self.request.translate
743 self.assure_not_empty_repo()
745 self.assure_not_empty_repo()
744
746
745 controls = peppercorn.parse(self.request.POST.items())
747 controls = peppercorn.parse(self.request.POST.items())
746
748
747 try:
749 try:
748 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
750 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
749 except formencode.Invalid as errors:
751 except formencode.Invalid as errors:
750 if errors.error_dict.get('revisions'):
752 if errors.error_dict.get('revisions'):
751 msg = 'Revisions: %s' % errors.error_dict['revisions']
753 msg = 'Revisions: %s' % errors.error_dict['revisions']
752 elif errors.error_dict.get('pullrequest_title'):
754 elif errors.error_dict.get('pullrequest_title'):
753 msg = _('Pull request requires a title with min. 3 chars')
755 msg = _('Pull request requires a title with min. 3 chars')
754 else:
756 else:
755 msg = _('Error creating pull request: {}').format(errors)
757 msg = _('Error creating pull request: {}').format(errors)
756 log.exception(msg)
758 log.exception(msg)
757 h.flash(msg, 'error')
759 h.flash(msg, 'error')
758
760
759 # would rather just go back to form ...
761 # would rather just go back to form ...
760 raise HTTPFound(
762 raise HTTPFound(
761 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
763 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
762
764
763 source_repo = _form['source_repo']
765 source_repo = _form['source_repo']
764 source_ref = _form['source_ref']
766 source_ref = _form['source_ref']
765 target_repo = _form['target_repo']
767 target_repo = _form['target_repo']
766 target_ref = _form['target_ref']
768 target_ref = _form['target_ref']
767 commit_ids = _form['revisions'][::-1]
769 commit_ids = _form['revisions'][::-1]
768
770
769 # find the ancestor for this pr
771 # find the ancestor for this pr
770 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
772 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
771 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
773 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
772
774
773 source_scm = source_db_repo.scm_instance()
775 source_scm = source_db_repo.scm_instance()
774 target_scm = target_db_repo.scm_instance()
776 target_scm = target_db_repo.scm_instance()
775
777
776 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
778 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
777 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
779 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
778
780
779 ancestor = source_scm.get_common_ancestor(
781 ancestor = source_scm.get_common_ancestor(
780 source_commit.raw_id, target_commit.raw_id, target_scm)
782 source_commit.raw_id, target_commit.raw_id, target_scm)
781
783
782 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
784 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
783 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
785 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
784
786
785 pullrequest_title = _form['pullrequest_title']
787 pullrequest_title = _form['pullrequest_title']
786 title_source_ref = source_ref.split(':', 2)[1]
788 title_source_ref = source_ref.split(':', 2)[1]
787 if not pullrequest_title:
789 if not pullrequest_title:
788 pullrequest_title = PullRequestModel().generate_pullrequest_title(
790 pullrequest_title = PullRequestModel().generate_pullrequest_title(
789 source=source_repo,
791 source=source_repo,
790 source_ref=title_source_ref,
792 source_ref=title_source_ref,
791 target=target_repo
793 target=target_repo
792 )
794 )
793
795
794 description = _form['pullrequest_desc']
796 description = _form['pullrequest_desc']
795
797
796 get_default_reviewers_data, validate_default_reviewers = \
798 get_default_reviewers_data, validate_default_reviewers = \
797 PullRequestModel().get_reviewer_functions()
799 PullRequestModel().get_reviewer_functions()
798
800
799 # recalculate reviewers logic, to make sure we can validate this
801 # recalculate reviewers logic, to make sure we can validate this
800 reviewer_rules = get_default_reviewers_data(
802 reviewer_rules = get_default_reviewers_data(
801 self._rhodecode_db_user, source_db_repo,
803 self._rhodecode_db_user, source_db_repo,
802 source_commit, target_db_repo, target_commit)
804 source_commit, target_db_repo, target_commit)
803
805
804 given_reviewers = _form['review_members']
806 given_reviewers = _form['review_members']
805 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
807 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
806
808
807 try:
809 try:
808 pull_request = PullRequestModel().create(
810 pull_request = PullRequestModel().create(
809 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
811 self._rhodecode_user.user_id, source_repo, source_ref,
810 target_ref, commit_ids, reviewers, pullrequest_title,
812 target_repo, target_ref, commit_ids, reviewers,
811 description, reviewer_rules
813 pullrequest_title, description, reviewer_rules
812 )
814 )
813 Session().commit()
815 Session().commit()
816
814 h.flash(_('Successfully opened new pull request'),
817 h.flash(_('Successfully opened new pull request'),
815 category='success')
818 category='success')
816 except Exception:
819 except Exception:
817 msg = _('Error occurred during creation of this pull request.')
820 msg = _('Error occurred during creation of this pull request.')
818 log.exception(msg)
821 log.exception(msg)
819 h.flash(msg, category='error')
822 h.flash(msg, category='error')
820
823
821 # copy the args back to redirect
824 # copy the args back to redirect
822 org_query = self.request.GET.mixed()
825 org_query = self.request.GET.mixed()
823 raise HTTPFound(
826 raise HTTPFound(
824 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
827 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
825 _query=org_query))
828 _query=org_query))
826
829
827 raise HTTPFound(
830 raise HTTPFound(
828 h.route_path('pullrequest_show', repo_name=target_repo,
831 h.route_path('pullrequest_show', repo_name=target_repo,
829 pull_request_id=pull_request.pull_request_id))
832 pull_request_id=pull_request.pull_request_id))
830
833
831 @LoginRequired()
834 @LoginRequired()
832 @NotAnonymous()
835 @NotAnonymous()
833 @HasRepoPermissionAnyDecorator(
836 @HasRepoPermissionAnyDecorator(
834 'repository.read', 'repository.write', 'repository.admin')
837 'repository.read', 'repository.write', 'repository.admin')
835 @CSRFRequired()
838 @CSRFRequired()
836 @view_config(
839 @view_config(
837 route_name='pullrequest_update', request_method='POST',
840 route_name='pullrequest_update', request_method='POST',
838 renderer='json_ext')
841 renderer='json_ext')
839 def pull_request_update(self):
842 def pull_request_update(self):
840 pull_request = PullRequest.get_or_404(
843 pull_request = PullRequest.get_or_404(
841 self.request.matchdict['pull_request_id'])
844 self.request.matchdict['pull_request_id'])
842
845
843 # only owner or admin can update it
846 # only owner or admin can update it
844 allowed_to_update = PullRequestModel().check_user_update(
847 allowed_to_update = PullRequestModel().check_user_update(
845 pull_request, self._rhodecode_user)
848 pull_request, self._rhodecode_user)
846 if allowed_to_update:
849 if allowed_to_update:
847 controls = peppercorn.parse(self.request.POST.items())
850 controls = peppercorn.parse(self.request.POST.items())
848
851
849 if 'review_members' in controls:
852 if 'review_members' in controls:
850 self._update_reviewers(
853 self._update_reviewers(
851 pull_request, controls['review_members'],
854 pull_request, controls['review_members'],
852 pull_request.reviewer_data)
855 pull_request.reviewer_data)
853 elif str2bool(self.request.POST.get('update_commits', 'false')):
856 elif str2bool(self.request.POST.get('update_commits', 'false')):
854 self._update_commits(pull_request)
857 self._update_commits(pull_request)
855 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
858 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
856 self._edit_pull_request(pull_request)
859 self._edit_pull_request(pull_request)
857 else:
860 else:
858 raise HTTPBadRequest()
861 raise HTTPBadRequest()
859 return True
862 return True
860 raise HTTPForbidden()
863 raise HTTPForbidden()
861
864
862 def _edit_pull_request(self, pull_request):
865 def _edit_pull_request(self, pull_request):
863 _ = self.request.translate
866 _ = self.request.translate
864 try:
867 try:
865 PullRequestModel().edit(
868 PullRequestModel().edit(
866 pull_request, self.request.POST.get('title'),
869 pull_request, self.request.POST.get('title'),
867 self.request.POST.get('description'), self._rhodecode_user)
870 self.request.POST.get('description'), self._rhodecode_user)
868 except ValueError:
871 except ValueError:
869 msg = _(u'Cannot update closed pull requests.')
872 msg = _(u'Cannot update closed pull requests.')
870 h.flash(msg, category='error')
873 h.flash(msg, category='error')
871 return
874 return
872 else:
875 else:
873 Session().commit()
876 Session().commit()
874
877
875 msg = _(u'Pull request title & description updated.')
878 msg = _(u'Pull request title & description updated.')
876 h.flash(msg, category='success')
879 h.flash(msg, category='success')
877 return
880 return
878
881
879 def _update_commits(self, pull_request):
882 def _update_commits(self, pull_request):
880 _ = self.request.translate
883 _ = self.request.translate
881 resp = PullRequestModel().update_commits(pull_request)
884 resp = PullRequestModel().update_commits(pull_request)
882
885
883 if resp.executed:
886 if resp.executed:
884
887
885 if resp.target_changed and resp.source_changed:
888 if resp.target_changed and resp.source_changed:
886 changed = 'target and source repositories'
889 changed = 'target and source repositories'
887 elif resp.target_changed and not resp.source_changed:
890 elif resp.target_changed and not resp.source_changed:
888 changed = 'target repository'
891 changed = 'target repository'
889 elif not resp.target_changed and resp.source_changed:
892 elif not resp.target_changed and resp.source_changed:
890 changed = 'source repository'
893 changed = 'source repository'
891 else:
894 else:
892 changed = 'nothing'
895 changed = 'nothing'
893
896
894 msg = _(
897 msg = _(
895 u'Pull request updated to "{source_commit_id}" with '
898 u'Pull request updated to "{source_commit_id}" with '
896 u'{count_added} added, {count_removed} removed commits. '
899 u'{count_added} added, {count_removed} removed commits. '
897 u'Source of changes: {change_source}')
900 u'Source of changes: {change_source}')
898 msg = msg.format(
901 msg = msg.format(
899 source_commit_id=pull_request.source_ref_parts.commit_id,
902 source_commit_id=pull_request.source_ref_parts.commit_id,
900 count_added=len(resp.changes.added),
903 count_added=len(resp.changes.added),
901 count_removed=len(resp.changes.removed),
904 count_removed=len(resp.changes.removed),
902 change_source=changed)
905 change_source=changed)
903 h.flash(msg, category='success')
906 h.flash(msg, category='success')
904
907
905 channel = '/repo${}$/pr/{}'.format(
908 channel = '/repo${}$/pr/{}'.format(
906 pull_request.target_repo.repo_name,
909 pull_request.target_repo.repo_name,
907 pull_request.pull_request_id)
910 pull_request.pull_request_id)
908 message = msg + (
911 message = msg + (
909 ' - <a onclick="window.location.reload()">'
912 ' - <a onclick="window.location.reload()">'
910 '<strong>{}</strong></a>'.format(_('Reload page')))
913 '<strong>{}</strong></a>'.format(_('Reload page')))
911 channelstream.post_message(
914 channelstream.post_message(
912 channel, message, self._rhodecode_user.username,
915 channel, message, self._rhodecode_user.username,
913 registry=self.request.registry)
916 registry=self.request.registry)
914 else:
917 else:
915 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
918 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
916 warning_reasons = [
919 warning_reasons = [
917 UpdateFailureReason.NO_CHANGE,
920 UpdateFailureReason.NO_CHANGE,
918 UpdateFailureReason.WRONG_REF_TYPE,
921 UpdateFailureReason.WRONG_REF_TYPE,
919 ]
922 ]
920 category = 'warning' if resp.reason in warning_reasons else 'error'
923 category = 'warning' if resp.reason in warning_reasons else 'error'
921 h.flash(msg, category=category)
924 h.flash(msg, category=category)
922
925
923 @LoginRequired()
926 @LoginRequired()
924 @NotAnonymous()
927 @NotAnonymous()
925 @HasRepoPermissionAnyDecorator(
928 @HasRepoPermissionAnyDecorator(
926 'repository.read', 'repository.write', 'repository.admin')
929 'repository.read', 'repository.write', 'repository.admin')
927 @CSRFRequired()
930 @CSRFRequired()
928 @view_config(
931 @view_config(
929 route_name='pullrequest_merge', request_method='POST',
932 route_name='pullrequest_merge', request_method='POST',
930 renderer='json_ext')
933 renderer='json_ext')
931 def pull_request_merge(self):
934 def pull_request_merge(self):
932 """
935 """
933 Merge will perform a server-side merge of the specified
936 Merge will perform a server-side merge of the specified
934 pull request, if the pull request is approved and mergeable.
937 pull request, if the pull request is approved and mergeable.
935 After successful merging, the pull request is automatically
938 After successful merging, the pull request is automatically
936 closed, with a relevant comment.
939 closed, with a relevant comment.
937 """
940 """
938 pull_request = PullRequest.get_or_404(
941 pull_request = PullRequest.get_or_404(
939 self.request.matchdict['pull_request_id'])
942 self.request.matchdict['pull_request_id'])
940
943
941 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
944 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
945 translator=self.request.translate)
942 merge_possible = not check.failed
946 merge_possible = not check.failed
943
947
944 for err_type, error_msg in check.errors:
948 for err_type, error_msg in check.errors:
945 h.flash(error_msg, category=err_type)
949 h.flash(error_msg, category=err_type)
946
950
947 if merge_possible:
951 if merge_possible:
948 log.debug("Pre-conditions checked, trying to merge.")
952 log.debug("Pre-conditions checked, trying to merge.")
949 extras = vcs_operation_context(
953 extras = vcs_operation_context(
950 self.request.environ, repo_name=pull_request.target_repo.repo_name,
954 self.request.environ, repo_name=pull_request.target_repo.repo_name,
951 username=self._rhodecode_db_user.username, action='push',
955 username=self._rhodecode_db_user.username, action='push',
952 scm=pull_request.target_repo.repo_type)
956 scm=pull_request.target_repo.repo_type)
953 self._merge_pull_request(
957 self._merge_pull_request(
954 pull_request, self._rhodecode_db_user, extras)
958 pull_request, self._rhodecode_db_user, extras)
955 else:
959 else:
956 log.debug("Pre-conditions failed, NOT merging.")
960 log.debug("Pre-conditions failed, NOT merging.")
957
961
958 raise HTTPFound(
962 raise HTTPFound(
959 h.route_path('pullrequest_show',
963 h.route_path('pullrequest_show',
960 repo_name=pull_request.target_repo.repo_name,
964 repo_name=pull_request.target_repo.repo_name,
961 pull_request_id=pull_request.pull_request_id))
965 pull_request_id=pull_request.pull_request_id))
962
966
963 def _merge_pull_request(self, pull_request, user, extras):
967 def _merge_pull_request(self, pull_request, user, extras):
964 _ = self.request.translate
968 _ = self.request.translate
965 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
969 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
966
970
967 if merge_resp.executed:
971 if merge_resp.executed:
968 log.debug("The merge was successful, closing the pull request.")
972 log.debug("The merge was successful, closing the pull request.")
969 PullRequestModel().close_pull_request(
973 PullRequestModel().close_pull_request(
970 pull_request.pull_request_id, user)
974 pull_request.pull_request_id, user)
971 Session().commit()
975 Session().commit()
972 msg = _('Pull request was successfully merged and closed.')
976 msg = _('Pull request was successfully merged and closed.')
973 h.flash(msg, category='success')
977 h.flash(msg, category='success')
974 else:
978 else:
975 log.debug(
979 log.debug(
976 "The merge was not successful. Merge response: %s",
980 "The merge was not successful. Merge response: %s",
977 merge_resp)
981 merge_resp)
978 msg = PullRequestModel().merge_status_message(
982 msg = PullRequestModel().merge_status_message(
979 merge_resp.failure_reason)
983 merge_resp.failure_reason)
980 h.flash(msg, category='error')
984 h.flash(msg, category='error')
981
985
982 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
986 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
983 _ = self.request.translate
987 _ = self.request.translate
984 get_default_reviewers_data, validate_default_reviewers = \
988 get_default_reviewers_data, validate_default_reviewers = \
985 PullRequestModel().get_reviewer_functions()
989 PullRequestModel().get_reviewer_functions()
986
990
987 try:
991 try:
988 reviewers = validate_default_reviewers(review_members, reviewer_rules)
992 reviewers = validate_default_reviewers(review_members, reviewer_rules)
989 except ValueError as e:
993 except ValueError as e:
990 log.error('Reviewers Validation: {}'.format(e))
994 log.error('Reviewers Validation: {}'.format(e))
991 h.flash(e, category='error')
995 h.flash(e, category='error')
992 return
996 return
993
997
994 PullRequestModel().update_reviewers(
998 PullRequestModel().update_reviewers(
995 pull_request, reviewers, self._rhodecode_user)
999 pull_request, reviewers, self._rhodecode_user)
996 h.flash(_('Pull request reviewers updated.'), category='success')
1000 h.flash(_('Pull request reviewers updated.'), category='success')
997 Session().commit()
1001 Session().commit()
998
1002
999 @LoginRequired()
1003 @LoginRequired()
1000 @NotAnonymous()
1004 @NotAnonymous()
1001 @HasRepoPermissionAnyDecorator(
1005 @HasRepoPermissionAnyDecorator(
1002 'repository.read', 'repository.write', 'repository.admin')
1006 'repository.read', 'repository.write', 'repository.admin')
1003 @CSRFRequired()
1007 @CSRFRequired()
1004 @view_config(
1008 @view_config(
1005 route_name='pullrequest_delete', request_method='POST',
1009 route_name='pullrequest_delete', request_method='POST',
1006 renderer='json_ext')
1010 renderer='json_ext')
1007 def pull_request_delete(self):
1011 def pull_request_delete(self):
1008 _ = self.request.translate
1012 _ = self.request.translate
1009
1013
1010 pull_request = PullRequest.get_or_404(
1014 pull_request = PullRequest.get_or_404(
1011 self.request.matchdict['pull_request_id'])
1015 self.request.matchdict['pull_request_id'])
1012
1016
1013 pr_closed = pull_request.is_closed()
1017 pr_closed = pull_request.is_closed()
1014 allowed_to_delete = PullRequestModel().check_user_delete(
1018 allowed_to_delete = PullRequestModel().check_user_delete(
1015 pull_request, self._rhodecode_user) and not pr_closed
1019 pull_request, self._rhodecode_user) and not pr_closed
1016
1020
1017 # only owner can delete it !
1021 # only owner can delete it !
1018 if allowed_to_delete:
1022 if allowed_to_delete:
1019 PullRequestModel().delete(pull_request, self._rhodecode_user)
1023 PullRequestModel().delete(pull_request, self._rhodecode_user)
1020 Session().commit()
1024 Session().commit()
1021 h.flash(_('Successfully deleted pull request'),
1025 h.flash(_('Successfully deleted pull request'),
1022 category='success')
1026 category='success')
1023 raise HTTPFound(h.route_path('pullrequest_show_all',
1027 raise HTTPFound(h.route_path('pullrequest_show_all',
1024 repo_name=self.db_repo_name))
1028 repo_name=self.db_repo_name))
1025
1029
1026 log.warning('user %s tried to delete pull request without access',
1030 log.warning('user %s tried to delete pull request without access',
1027 self._rhodecode_user)
1031 self._rhodecode_user)
1028 raise HTTPNotFound()
1032 raise HTTPNotFound()
1029
1033
1030 @LoginRequired()
1034 @LoginRequired()
1031 @NotAnonymous()
1035 @NotAnonymous()
1032 @HasRepoPermissionAnyDecorator(
1036 @HasRepoPermissionAnyDecorator(
1033 'repository.read', 'repository.write', 'repository.admin')
1037 'repository.read', 'repository.write', 'repository.admin')
1034 @CSRFRequired()
1038 @CSRFRequired()
1035 @view_config(
1039 @view_config(
1036 route_name='pullrequest_comment_create', request_method='POST',
1040 route_name='pullrequest_comment_create', request_method='POST',
1037 renderer='json_ext')
1041 renderer='json_ext')
1038 def pull_request_comment_create(self):
1042 def pull_request_comment_create(self):
1039 _ = self.request.translate
1043 _ = self.request.translate
1040
1044
1041 pull_request = PullRequest.get_or_404(
1045 pull_request = PullRequest.get_or_404(
1042 self.request.matchdict['pull_request_id'])
1046 self.request.matchdict['pull_request_id'])
1043 pull_request_id = pull_request.pull_request_id
1047 pull_request_id = pull_request.pull_request_id
1044
1048
1045 if pull_request.is_closed():
1049 if pull_request.is_closed():
1046 log.debug('comment: forbidden because pull request is closed')
1050 log.debug('comment: forbidden because pull request is closed')
1047 raise HTTPForbidden()
1051 raise HTTPForbidden()
1048
1052
1049 c = self.load_default_context()
1053 c = self.load_default_context()
1050
1054
1051 status = self.request.POST.get('changeset_status', None)
1055 status = self.request.POST.get('changeset_status', None)
1052 text = self.request.POST.get('text')
1056 text = self.request.POST.get('text')
1053 comment_type = self.request.POST.get('comment_type')
1057 comment_type = self.request.POST.get('comment_type')
1054 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1058 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1055 close_pull_request = self.request.POST.get('close_pull_request')
1059 close_pull_request = self.request.POST.get('close_pull_request')
1056
1060
1057 # the logic here should work like following, if we submit close
1061 # the logic here should work like following, if we submit close
1058 # pr comment, use `close_pull_request_with_comment` function
1062 # pr comment, use `close_pull_request_with_comment` function
1059 # else handle regular comment logic
1063 # else handle regular comment logic
1060
1064
1061 if close_pull_request:
1065 if close_pull_request:
1062 # only owner or admin or person with write permissions
1066 # only owner or admin or person with write permissions
1063 allowed_to_close = PullRequestModel().check_user_update(
1067 allowed_to_close = PullRequestModel().check_user_update(
1064 pull_request, self._rhodecode_user)
1068 pull_request, self._rhodecode_user)
1065 if not allowed_to_close:
1069 if not allowed_to_close:
1066 log.debug('comment: forbidden because not allowed to close '
1070 log.debug('comment: forbidden because not allowed to close '
1067 'pull request %s', pull_request_id)
1071 'pull request %s', pull_request_id)
1068 raise HTTPForbidden()
1072 raise HTTPForbidden()
1069 comment, status = PullRequestModel().close_pull_request_with_comment(
1073 comment, status = PullRequestModel().close_pull_request_with_comment(
1070 pull_request, self._rhodecode_user, self.db_repo, message=text)
1074 pull_request, self._rhodecode_user, self.db_repo, message=text)
1071 Session().flush()
1075 Session().flush()
1072 events.trigger(
1076 events.trigger(
1073 events.PullRequestCommentEvent(pull_request, comment))
1077 events.PullRequestCommentEvent(pull_request, comment))
1074
1078
1075 else:
1079 else:
1076 # regular comment case, could be inline, or one with status.
1080 # regular comment case, could be inline, or one with status.
1077 # for that one we check also permissions
1081 # for that one we check also permissions
1078
1082
1079 allowed_to_change_status = PullRequestModel().check_user_change_status(
1083 allowed_to_change_status = PullRequestModel().check_user_change_status(
1080 pull_request, self._rhodecode_user)
1084 pull_request, self._rhodecode_user)
1081
1085
1082 if status and allowed_to_change_status:
1086 if status and allowed_to_change_status:
1083 message = (_('Status change %(transition_icon)s %(status)s')
1087 message = (_('Status change %(transition_icon)s %(status)s')
1084 % {'transition_icon': '>',
1088 % {'transition_icon': '>',
1085 'status': ChangesetStatus.get_status_lbl(status)})
1089 'status': ChangesetStatus.get_status_lbl(status)})
1086 text = text or message
1090 text = text or message
1087
1091
1088 comment = CommentsModel().create(
1092 comment = CommentsModel().create(
1089 text=text,
1093 text=text,
1090 repo=self.db_repo.repo_id,
1094 repo=self.db_repo.repo_id,
1091 user=self._rhodecode_user.user_id,
1095 user=self._rhodecode_user.user_id,
1092 pull_request=pull_request,
1096 pull_request=pull_request,
1093 f_path=self.request.POST.get('f_path'),
1097 f_path=self.request.POST.get('f_path'),
1094 line_no=self.request.POST.get('line'),
1098 line_no=self.request.POST.get('line'),
1095 status_change=(ChangesetStatus.get_status_lbl(status)
1099 status_change=(ChangesetStatus.get_status_lbl(status)
1096 if status and allowed_to_change_status else None),
1100 if status and allowed_to_change_status else None),
1097 status_change_type=(status
1101 status_change_type=(status
1098 if status and allowed_to_change_status else None),
1102 if status and allowed_to_change_status else None),
1099 comment_type=comment_type,
1103 comment_type=comment_type,
1100 resolves_comment_id=resolves_comment_id
1104 resolves_comment_id=resolves_comment_id
1101 )
1105 )
1102
1106
1103 if allowed_to_change_status:
1107 if allowed_to_change_status:
1104 # calculate old status before we change it
1108 # calculate old status before we change it
1105 old_calculated_status = pull_request.calculated_review_status()
1109 old_calculated_status = pull_request.calculated_review_status()
1106
1110
1107 # get status if set !
1111 # get status if set !
1108 if status:
1112 if status:
1109 ChangesetStatusModel().set_status(
1113 ChangesetStatusModel().set_status(
1110 self.db_repo.repo_id,
1114 self.db_repo.repo_id,
1111 status,
1115 status,
1112 self._rhodecode_user.user_id,
1116 self._rhodecode_user.user_id,
1113 comment,
1117 comment,
1114 pull_request=pull_request
1118 pull_request=pull_request
1115 )
1119 )
1116
1120
1117 Session().flush()
1121 Session().flush()
1118 events.trigger(
1122 events.trigger(
1119 events.PullRequestCommentEvent(pull_request, comment))
1123 events.PullRequestCommentEvent(pull_request, comment))
1120
1124
1121 # we now calculate the status of pull request, and based on that
1125 # we now calculate the status of pull request, and based on that
1122 # calculation we set the commits status
1126 # calculation we set the commits status
1123 calculated_status = pull_request.calculated_review_status()
1127 calculated_status = pull_request.calculated_review_status()
1124 if old_calculated_status != calculated_status:
1128 if old_calculated_status != calculated_status:
1125 PullRequestModel()._trigger_pull_request_hook(
1129 PullRequestModel()._trigger_pull_request_hook(
1126 pull_request, self._rhodecode_user, 'review_status_change')
1130 pull_request, self._rhodecode_user, 'review_status_change')
1127
1131
1128 Session().commit()
1132 Session().commit()
1129
1133
1130 data = {
1134 data = {
1131 'target_id': h.safeid(h.safe_unicode(
1135 'target_id': h.safeid(h.safe_unicode(
1132 self.request.POST.get('f_path'))),
1136 self.request.POST.get('f_path'))),
1133 }
1137 }
1134 if comment:
1138 if comment:
1135 c.co = comment
1139 c.co = comment
1136 rendered_comment = render(
1140 rendered_comment = render(
1137 'rhodecode:templates/changeset/changeset_comment_block.mako',
1141 'rhodecode:templates/changeset/changeset_comment_block.mako',
1138 self._get_template_context(c), self.request)
1142 self._get_template_context(c), self.request)
1139
1143
1140 data.update(comment.get_dict())
1144 data.update(comment.get_dict())
1141 data.update({'rendered_text': rendered_comment})
1145 data.update({'rendered_text': rendered_comment})
1142
1146
1143 return data
1147 return data
1144
1148
1145 @LoginRequired()
1149 @LoginRequired()
1146 @NotAnonymous()
1150 @NotAnonymous()
1147 @HasRepoPermissionAnyDecorator(
1151 @HasRepoPermissionAnyDecorator(
1148 'repository.read', 'repository.write', 'repository.admin')
1152 'repository.read', 'repository.write', 'repository.admin')
1149 @CSRFRequired()
1153 @CSRFRequired()
1150 @view_config(
1154 @view_config(
1151 route_name='pullrequest_comment_delete', request_method='POST',
1155 route_name='pullrequest_comment_delete', request_method='POST',
1152 renderer='json_ext')
1156 renderer='json_ext')
1153 def pull_request_comment_delete(self):
1157 def pull_request_comment_delete(self):
1154 pull_request = PullRequest.get_or_404(
1158 pull_request = PullRequest.get_or_404(
1155 self.request.matchdict['pull_request_id'])
1159 self.request.matchdict['pull_request_id'])
1156
1160
1157 comment = ChangesetComment.get_or_404(
1161 comment = ChangesetComment.get_or_404(
1158 self.request.matchdict['comment_id'])
1162 self.request.matchdict['comment_id'])
1159 comment_id = comment.comment_id
1163 comment_id = comment.comment_id
1160
1164
1161 if pull_request.is_closed():
1165 if pull_request.is_closed():
1162 log.debug('comment: forbidden because pull request is closed')
1166 log.debug('comment: forbidden because pull request is closed')
1163 raise HTTPForbidden()
1167 raise HTTPForbidden()
1164
1168
1165 if not comment:
1169 if not comment:
1166 log.debug('Comment with id:%s not found, skipping', comment_id)
1170 log.debug('Comment with id:%s not found, skipping', comment_id)
1167 # comment already deleted in another call probably
1171 # comment already deleted in another call probably
1168 return True
1172 return True
1169
1173
1170 if comment.pull_request.is_closed():
1174 if comment.pull_request.is_closed():
1171 # don't allow deleting comments on closed pull request
1175 # don't allow deleting comments on closed pull request
1172 raise HTTPForbidden()
1176 raise HTTPForbidden()
1173
1177
1174 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1178 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1175 super_admin = h.HasPermissionAny('hg.admin')()
1179 super_admin = h.HasPermissionAny('hg.admin')()
1176 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1180 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1177 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1181 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1178 comment_repo_admin = is_repo_admin and is_repo_comment
1182 comment_repo_admin = is_repo_admin and is_repo_comment
1179
1183
1180 if super_admin or comment_owner or comment_repo_admin:
1184 if super_admin or comment_owner or comment_repo_admin:
1181 old_calculated_status = comment.pull_request.calculated_review_status()
1185 old_calculated_status = comment.pull_request.calculated_review_status()
1182 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1186 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1183 Session().commit()
1187 Session().commit()
1184 calculated_status = comment.pull_request.calculated_review_status()
1188 calculated_status = comment.pull_request.calculated_review_status()
1185 if old_calculated_status != calculated_status:
1189 if old_calculated_status != calculated_status:
1186 PullRequestModel()._trigger_pull_request_hook(
1190 PullRequestModel()._trigger_pull_request_hook(
1187 comment.pull_request, self._rhodecode_user, 'review_status_change')
1191 comment.pull_request, self._rhodecode_user, 'review_status_change')
1188 return True
1192 return True
1189 else:
1193 else:
1190 log.warning('No permissions for user %s to delete comment_id: %s',
1194 log.warning('No permissions for user %s to delete comment_id: %s',
1191 self._rhodecode_db_user, comment_id)
1195 self._rhodecode_db_user, comment_id)
1192 raise HTTPNotFound()
1196 raise HTTPNotFound()
@@ -1,631 +1,635 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32 import pyramid.threadlocal
32 import pyramid.threadlocal
33
33
34 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37
37
38 import rhodecode
38 import rhodecode
39 from rhodecode.authentication.base import VCS_TYPE
39 from rhodecode.authentication.base import VCS_TYPE
40 from rhodecode.lib import auth, utils2
40 from rhodecode.lib import auth, utils2
41 from rhodecode.lib import helpers as h
41 from rhodecode.lib import helpers as h
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.exceptions import UserCreationError
44 from rhodecode.lib.utils import (
44 from rhodecode.lib.utils import (
45 get_repo_slug, set_rhodecode_config, password_changed,
45 get_repo_slug, set_rhodecode_config, password_changed,
46 get_enabled_hook_classes)
46 get_enabled_hook_classes)
47 from rhodecode.lib.utils2 import (
47 from rhodecode.lib.utils2 import (
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
49 from rhodecode.model import meta
49 from rhodecode.model import meta
50 from rhodecode.model.db import Repository, User, ChangesetComment
50 from rhodecode.model.db import Repository, User, ChangesetComment
51 from rhodecode.model.notification import NotificationModel
51 from rhodecode.model.notification import NotificationModel
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54
54
55 # NOTE(marcink): remove after base controller is no longer required
55 # NOTE(marcink): remove after base controller is no longer required
56 from pylons.controllers import WSGIController
56 from pylons.controllers import WSGIController
57 from pylons.i18n import translation
57 from pylons.i18n import translation
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 # hack to make the migration to pyramid easier
62 # hack to make the migration to pyramid easier
63 def render(template_name, extra_vars=None, cache_key=None,
63 def render(template_name, extra_vars=None, cache_key=None,
64 cache_type=None, cache_expire=None):
64 cache_type=None, cache_expire=None):
65 """Render a template with Mako
65 """Render a template with Mako
66
66
67 Accepts the cache options ``cache_key``, ``cache_type``, and
67 Accepts the cache options ``cache_key``, ``cache_type``, and
68 ``cache_expire``.
68 ``cache_expire``.
69
69
70 """
70 """
71 from pylons.templating import literal
71 from pylons.templating import literal
72 from pylons.templating import cached_template, pylons_globals
72 from pylons.templating import cached_template, pylons_globals
73
73
74 # Create a render callable for the cache function
74 # Create a render callable for the cache function
75 def render_template():
75 def render_template():
76 # Pull in extra vars if needed
76 # Pull in extra vars if needed
77 globs = extra_vars or {}
77 globs = extra_vars or {}
78
78
79 # Second, get the globals
79 # Second, get the globals
80 globs.update(pylons_globals())
80 globs.update(pylons_globals())
81
81
82 globs['_ungettext'] = globs['ungettext']
82 globs['_ungettext'] = globs['ungettext']
83 # Grab a template reference
83 # Grab a template reference
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
85
85
86 return literal(template.render_unicode(**globs))
86 return literal(template.render_unicode(**globs))
87
87
88 return cached_template(template_name, render_template, cache_key=cache_key,
88 return cached_template(template_name, render_template, cache_key=cache_key,
89 cache_type=cache_type, cache_expire=cache_expire)
89 cache_type=cache_type, cache_expire=cache_expire)
90
90
91 def _filter_proxy(ip):
91 def _filter_proxy(ip):
92 """
92 """
93 Passed in IP addresses in HEADERS can be in a special format of multiple
93 Passed in IP addresses in HEADERS can be in a special format of multiple
94 ips. Those comma separated IPs are passed from various proxies in the
94 ips. Those comma separated IPs are passed from various proxies in the
95 chain of request processing. The left-most being the original client.
95 chain of request processing. The left-most being the original client.
96 We only care about the first IP which came from the org. client.
96 We only care about the first IP which came from the org. client.
97
97
98 :param ip: ip string from headers
98 :param ip: ip string from headers
99 """
99 """
100 if ',' in ip:
100 if ',' in ip:
101 _ips = ip.split(',')
101 _ips = ip.split(',')
102 _first_ip = _ips[0].strip()
102 _first_ip = _ips[0].strip()
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
104 return _first_ip
104 return _first_ip
105 return ip
105 return ip
106
106
107
107
108 def _filter_port(ip):
108 def _filter_port(ip):
109 """
109 """
110 Removes a port from ip, there are 4 main cases to handle here.
110 Removes a port from ip, there are 4 main cases to handle here.
111 - ipv4 eg. 127.0.0.1
111 - ipv4 eg. 127.0.0.1
112 - ipv6 eg. ::1
112 - ipv6 eg. ::1
113 - ipv4+port eg. 127.0.0.1:8080
113 - ipv4+port eg. 127.0.0.1:8080
114 - ipv6+port eg. [::1]:8080
114 - ipv6+port eg. [::1]:8080
115
115
116 :param ip:
116 :param ip:
117 """
117 """
118 def is_ipv6(ip_addr):
118 def is_ipv6(ip_addr):
119 if hasattr(socket, 'inet_pton'):
119 if hasattr(socket, 'inet_pton'):
120 try:
120 try:
121 socket.inet_pton(socket.AF_INET6, ip_addr)
121 socket.inet_pton(socket.AF_INET6, ip_addr)
122 except socket.error:
122 except socket.error:
123 return False
123 return False
124 else:
124 else:
125 # fallback to ipaddress
125 # fallback to ipaddress
126 try:
126 try:
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
128 except Exception:
128 except Exception:
129 return False
129 return False
130 return True
130 return True
131
131
132 if ':' not in ip: # must be ipv4 pure ip
132 if ':' not in ip: # must be ipv4 pure ip
133 return ip
133 return ip
134
134
135 if '[' in ip and ']' in ip: # ipv6 with port
135 if '[' in ip and ']' in ip: # ipv6 with port
136 return ip.split(']')[0][1:].lower()
136 return ip.split(']')[0][1:].lower()
137
137
138 # must be ipv6 or ipv4 with port
138 # must be ipv6 or ipv4 with port
139 if is_ipv6(ip):
139 if is_ipv6(ip):
140 return ip
140 return ip
141 else:
141 else:
142 ip, _port = ip.split(':')[:2] # means ipv4+port
142 ip, _port = ip.split(':')[:2] # means ipv4+port
143 return ip
143 return ip
144
144
145
145
146 def get_ip_addr(environ):
146 def get_ip_addr(environ):
147 proxy_key = 'HTTP_X_REAL_IP'
147 proxy_key = 'HTTP_X_REAL_IP'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
149 def_key = 'REMOTE_ADDR'
149 def_key = 'REMOTE_ADDR'
150 _filters = lambda x: _filter_port(_filter_proxy(x))
150 _filters = lambda x: _filter_port(_filter_proxy(x))
151
151
152 ip = environ.get(proxy_key)
152 ip = environ.get(proxy_key)
153 if ip:
153 if ip:
154 return _filters(ip)
154 return _filters(ip)
155
155
156 ip = environ.get(proxy_key2)
156 ip = environ.get(proxy_key2)
157 if ip:
157 if ip:
158 return _filters(ip)
158 return _filters(ip)
159
159
160 ip = environ.get(def_key, '0.0.0.0')
160 ip = environ.get(def_key, '0.0.0.0')
161 return _filters(ip)
161 return _filters(ip)
162
162
163
163
164 def get_server_ip_addr(environ, log_errors=True):
164 def get_server_ip_addr(environ, log_errors=True):
165 hostname = environ.get('SERVER_NAME')
165 hostname = environ.get('SERVER_NAME')
166 try:
166 try:
167 return socket.gethostbyname(hostname)
167 return socket.gethostbyname(hostname)
168 except Exception as e:
168 except Exception as e:
169 if log_errors:
169 if log_errors:
170 # in some cases this lookup is not possible, and we don't want to
170 # in some cases this lookup is not possible, and we don't want to
171 # make it an exception in logs
171 # make it an exception in logs
172 log.exception('Could not retrieve server ip address: %s', e)
172 log.exception('Could not retrieve server ip address: %s', e)
173 return hostname
173 return hostname
174
174
175
175
176 def get_server_port(environ):
176 def get_server_port(environ):
177 return environ.get('SERVER_PORT')
177 return environ.get('SERVER_PORT')
178
178
179
179
180 def get_access_path(environ):
180 def get_access_path(environ):
181 path = environ.get('PATH_INFO')
181 path = environ.get('PATH_INFO')
182 org_req = environ.get('pylons.original_request')
182 org_req = environ.get('pylons.original_request')
183 if org_req:
183 if org_req:
184 path = org_req.environ.get('PATH_INFO')
184 path = org_req.environ.get('PATH_INFO')
185 return path
185 return path
186
186
187
187
188 def get_user_agent(environ):
188 def get_user_agent(environ):
189 return environ.get('HTTP_USER_AGENT')
189 return environ.get('HTTP_USER_AGENT')
190
190
191
191
192 def vcs_operation_context(
192 def vcs_operation_context(
193 environ, repo_name, username, action, scm, check_locking=True,
193 environ, repo_name, username, action, scm, check_locking=True,
194 is_shadow_repo=False):
194 is_shadow_repo=False):
195 """
195 """
196 Generate the context for a vcs operation, e.g. push or pull.
196 Generate the context for a vcs operation, e.g. push or pull.
197
197
198 This context is passed over the layers so that hooks triggered by the
198 This context is passed over the layers so that hooks triggered by the
199 vcs operation know details like the user, the user's IP address etc.
199 vcs operation know details like the user, the user's IP address etc.
200
200
201 :param check_locking: Allows to switch of the computation of the locking
201 :param check_locking: Allows to switch of the computation of the locking
202 data. This serves mainly the need of the simplevcs middleware to be
202 data. This serves mainly the need of the simplevcs middleware to be
203 able to disable this for certain operations.
203 able to disable this for certain operations.
204
204
205 """
205 """
206 # Tri-state value: False: unlock, None: nothing, True: lock
206 # Tri-state value: False: unlock, None: nothing, True: lock
207 make_lock = None
207 make_lock = None
208 locked_by = [None, None, None]
208 locked_by = [None, None, None]
209 is_anonymous = username == User.DEFAULT_USER
209 is_anonymous = username == User.DEFAULT_USER
210 if not is_anonymous and check_locking:
210 if not is_anonymous and check_locking:
211 log.debug('Checking locking on repository "%s"', repo_name)
211 log.debug('Checking locking on repository "%s"', repo_name)
212 user = User.get_by_username(username)
212 user = User.get_by_username(username)
213 repo = Repository.get_by_repo_name(repo_name)
213 repo = Repository.get_by_repo_name(repo_name)
214 make_lock, __, locked_by = repo.get_locking_state(
214 make_lock, __, locked_by = repo.get_locking_state(
215 action, user.user_id)
215 action, user.user_id)
216
216
217 settings_model = VcsSettingsModel(repo=repo_name)
217 settings_model = VcsSettingsModel(repo=repo_name)
218 ui_settings = settings_model.get_ui_settings()
218 ui_settings = settings_model.get_ui_settings()
219
219
220 extras = {
220 extras = {
221 'ip': get_ip_addr(environ),
221 'ip': get_ip_addr(environ),
222 'username': username,
222 'username': username,
223 'action': action,
223 'action': action,
224 'repository': repo_name,
224 'repository': repo_name,
225 'scm': scm,
225 'scm': scm,
226 'config': rhodecode.CONFIG['__file__'],
226 'config': rhodecode.CONFIG['__file__'],
227 'make_lock': make_lock,
227 'make_lock': make_lock,
228 'locked_by': locked_by,
228 'locked_by': locked_by,
229 'server_url': utils2.get_server_url(environ),
229 'server_url': utils2.get_server_url(environ),
230 'user_agent': get_user_agent(environ),
230 'user_agent': get_user_agent(environ),
231 'hooks': get_enabled_hook_classes(ui_settings),
231 'hooks': get_enabled_hook_classes(ui_settings),
232 'is_shadow_repo': is_shadow_repo,
232 'is_shadow_repo': is_shadow_repo,
233 }
233 }
234 return extras
234 return extras
235
235
236
236
237 class BasicAuth(AuthBasicAuthenticator):
237 class BasicAuth(AuthBasicAuthenticator):
238
238
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
240 initial_call_detection=False, acl_repo_name=None):
240 initial_call_detection=False, acl_repo_name=None):
241 self.realm = realm
241 self.realm = realm
242 self.initial_call = initial_call_detection
242 self.initial_call = initial_call_detection
243 self.authfunc = authfunc
243 self.authfunc = authfunc
244 self.registry = registry
244 self.registry = registry
245 self.acl_repo_name = acl_repo_name
245 self.acl_repo_name = acl_repo_name
246 self._rc_auth_http_code = auth_http_code
246 self._rc_auth_http_code = auth_http_code
247
247
248 def _get_response_from_code(self, http_code):
248 def _get_response_from_code(self, http_code):
249 try:
249 try:
250 return get_exception(safe_int(http_code))
250 return get_exception(safe_int(http_code))
251 except Exception:
251 except Exception:
252 log.exception('Failed to fetch response for code %s' % http_code)
252 log.exception('Failed to fetch response for code %s' % http_code)
253 return HTTPForbidden
253 return HTTPForbidden
254
254
255 def get_rc_realm(self):
255 def get_rc_realm(self):
256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
257
257
258 def build_authentication(self):
258 def build_authentication(self):
259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
260 if self._rc_auth_http_code and not self.initial_call:
260 if self._rc_auth_http_code and not self.initial_call:
261 # return alternative HTTP code if alternative http return code
261 # return alternative HTTP code if alternative http return code
262 # is specified in RhodeCode config, but ONLY if it's not the
262 # is specified in RhodeCode config, but ONLY if it's not the
263 # FIRST call
263 # FIRST call
264 custom_response_klass = self._get_response_from_code(
264 custom_response_klass = self._get_response_from_code(
265 self._rc_auth_http_code)
265 self._rc_auth_http_code)
266 return custom_response_klass(headers=head)
266 return custom_response_klass(headers=head)
267 return HTTPUnauthorized(headers=head)
267 return HTTPUnauthorized(headers=head)
268
268
269 def authenticate(self, environ):
269 def authenticate(self, environ):
270 authorization = AUTHORIZATION(environ)
270 authorization = AUTHORIZATION(environ)
271 if not authorization:
271 if not authorization:
272 return self.build_authentication()
272 return self.build_authentication()
273 (authmeth, auth) = authorization.split(' ', 1)
273 (authmeth, auth) = authorization.split(' ', 1)
274 if 'basic' != authmeth.lower():
274 if 'basic' != authmeth.lower():
275 return self.build_authentication()
275 return self.build_authentication()
276 auth = auth.strip().decode('base64')
276 auth = auth.strip().decode('base64')
277 _parts = auth.split(':', 1)
277 _parts = auth.split(':', 1)
278 if len(_parts) == 2:
278 if len(_parts) == 2:
279 username, password = _parts
279 username, password = _parts
280 auth_data = self.authfunc(
280 auth_data = self.authfunc(
281 username, password, environ, VCS_TYPE,
281 username, password, environ, VCS_TYPE,
282 registry=self.registry, acl_repo_name=self.acl_repo_name)
282 registry=self.registry, acl_repo_name=self.acl_repo_name)
283 if auth_data:
283 if auth_data:
284 return {'username': username, 'auth_data': auth_data}
284 return {'username': username, 'auth_data': auth_data}
285 if username and password:
285 if username and password:
286 # we mark that we actually executed authentication once, at
286 # we mark that we actually executed authentication once, at
287 # that point we can use the alternative auth code
287 # that point we can use the alternative auth code
288 self.initial_call = False
288 self.initial_call = False
289
289
290 return self.build_authentication()
290 return self.build_authentication()
291
291
292 __call__ = authenticate
292 __call__ = authenticate
293
293
294
294
295 def calculate_version_hash(config):
295 def calculate_version_hash(config):
296 return md5(
296 return md5(
297 config.get('beaker.session.secret', '') +
297 config.get('beaker.session.secret', '') +
298 rhodecode.__version__)[:8]
298 rhodecode.__version__)[:8]
299
299
300
300
301 def get_current_lang(request):
301 def get_current_lang(request):
302 # NOTE(marcink): remove after pyramid move
302 # NOTE(marcink): remove after pyramid move
303 try:
303 try:
304 return translation.get_lang()[0]
304 return translation.get_lang()[0]
305 except:
305 except:
306 pass
306 pass
307
307
308 return getattr(request, '_LOCALE_', request.locale_name)
308 return getattr(request, '_LOCALE_', request.locale_name)
309
309
310
310
311 def attach_context_attributes(context, request, user_id):
311 def attach_context_attributes(context, request, user_id):
312 """
312 """
313 Attach variables into template context called `c`, please note that
313 Attach variables into template context called `c`, please note that
314 request could be pylons or pyramid request in here.
314 request could be pylons or pyramid request in here.
315 """
315 """
316 # NOTE(marcink): remove check after pyramid migration
316 # NOTE(marcink): remove check after pyramid migration
317 if hasattr(request, 'registry'):
317 if hasattr(request, 'registry'):
318 config = request.registry.settings
318 config = request.registry.settings
319 else:
319 else:
320 from pylons import config
320 from pylons import config
321
321
322 rc_config = SettingsModel().get_all_settings(cache=True)
322 rc_config = SettingsModel().get_all_settings(cache=True)
323
323
324 context.rhodecode_version = rhodecode.__version__
324 context.rhodecode_version = rhodecode.__version__
325 context.rhodecode_edition = config.get('rhodecode.edition')
325 context.rhodecode_edition = config.get('rhodecode.edition')
326 # unique secret + version does not leak the version but keep consistency
326 # unique secret + version does not leak the version but keep consistency
327 context.rhodecode_version_hash = calculate_version_hash(config)
327 context.rhodecode_version_hash = calculate_version_hash(config)
328
328
329 # Default language set for the incoming request
329 # Default language set for the incoming request
330 context.language = get_current_lang(request)
330 context.language = get_current_lang(request)
331
331
332 # Visual options
332 # Visual options
333 context.visual = AttributeDict({})
333 context.visual = AttributeDict({})
334
334
335 # DB stored Visual Items
335 # DB stored Visual Items
336 context.visual.show_public_icon = str2bool(
336 context.visual.show_public_icon = str2bool(
337 rc_config.get('rhodecode_show_public_icon'))
337 rc_config.get('rhodecode_show_public_icon'))
338 context.visual.show_private_icon = str2bool(
338 context.visual.show_private_icon = str2bool(
339 rc_config.get('rhodecode_show_private_icon'))
339 rc_config.get('rhodecode_show_private_icon'))
340 context.visual.stylify_metatags = str2bool(
340 context.visual.stylify_metatags = str2bool(
341 rc_config.get('rhodecode_stylify_metatags'))
341 rc_config.get('rhodecode_stylify_metatags'))
342 context.visual.dashboard_items = safe_int(
342 context.visual.dashboard_items = safe_int(
343 rc_config.get('rhodecode_dashboard_items', 100))
343 rc_config.get('rhodecode_dashboard_items', 100))
344 context.visual.admin_grid_items = safe_int(
344 context.visual.admin_grid_items = safe_int(
345 rc_config.get('rhodecode_admin_grid_items', 100))
345 rc_config.get('rhodecode_admin_grid_items', 100))
346 context.visual.repository_fields = str2bool(
346 context.visual.repository_fields = str2bool(
347 rc_config.get('rhodecode_repository_fields'))
347 rc_config.get('rhodecode_repository_fields'))
348 context.visual.show_version = str2bool(
348 context.visual.show_version = str2bool(
349 rc_config.get('rhodecode_show_version'))
349 rc_config.get('rhodecode_show_version'))
350 context.visual.use_gravatar = str2bool(
350 context.visual.use_gravatar = str2bool(
351 rc_config.get('rhodecode_use_gravatar'))
351 rc_config.get('rhodecode_use_gravatar'))
352 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
352 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
353 context.visual.default_renderer = rc_config.get(
353 context.visual.default_renderer = rc_config.get(
354 'rhodecode_markup_renderer', 'rst')
354 'rhodecode_markup_renderer', 'rst')
355 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
355 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
356 context.visual.rhodecode_support_url = \
356 context.visual.rhodecode_support_url = \
357 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
357 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
358
358
359 context.visual.affected_files_cut_off = 60
359 context.visual.affected_files_cut_off = 60
360
360
361 context.pre_code = rc_config.get('rhodecode_pre_code')
361 context.pre_code = rc_config.get('rhodecode_pre_code')
362 context.post_code = rc_config.get('rhodecode_post_code')
362 context.post_code = rc_config.get('rhodecode_post_code')
363 context.rhodecode_name = rc_config.get('rhodecode_title')
363 context.rhodecode_name = rc_config.get('rhodecode_title')
364 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
364 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
365 # if we have specified default_encoding in the request, it has more
365 # if we have specified default_encoding in the request, it has more
366 # priority
366 # priority
367 if request.GET.get('default_encoding'):
367 if request.GET.get('default_encoding'):
368 context.default_encodings.insert(0, request.GET.get('default_encoding'))
368 context.default_encodings.insert(0, request.GET.get('default_encoding'))
369 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
369 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
370
370
371 # INI stored
371 # INI stored
372 context.labs_active = str2bool(
372 context.labs_active = str2bool(
373 config.get('labs_settings_active', 'false'))
373 config.get('labs_settings_active', 'false'))
374 context.visual.allow_repo_location_change = str2bool(
374 context.visual.allow_repo_location_change = str2bool(
375 config.get('allow_repo_location_change', True))
375 config.get('allow_repo_location_change', True))
376 context.visual.allow_custom_hooks_settings = str2bool(
376 context.visual.allow_custom_hooks_settings = str2bool(
377 config.get('allow_custom_hooks_settings', True))
377 config.get('allow_custom_hooks_settings', True))
378 context.debug_style = str2bool(config.get('debug_style', False))
378 context.debug_style = str2bool(config.get('debug_style', False))
379
379
380 context.rhodecode_instanceid = config.get('instance_id')
380 context.rhodecode_instanceid = config.get('instance_id')
381
381
382 context.visual.cut_off_limit_diff = safe_int(
382 context.visual.cut_off_limit_diff = safe_int(
383 config.get('cut_off_limit_diff'))
383 config.get('cut_off_limit_diff'))
384 context.visual.cut_off_limit_file = safe_int(
384 context.visual.cut_off_limit_file = safe_int(
385 config.get('cut_off_limit_file'))
385 config.get('cut_off_limit_file'))
386
386
387 # AppEnlight
387 # AppEnlight
388 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
388 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
389 context.appenlight_api_public_key = config.get(
389 context.appenlight_api_public_key = config.get(
390 'appenlight.api_public_key', '')
390 'appenlight.api_public_key', '')
391 context.appenlight_server_url = config.get('appenlight.server_url', '')
391 context.appenlight_server_url = config.get('appenlight.server_url', '')
392
392
393 # JS template context
393 # JS template context
394 context.template_context = {
394 context.template_context = {
395 'repo_name': None,
395 'repo_name': None,
396 'repo_type': None,
396 'repo_type': None,
397 'repo_landing_commit': None,
397 'repo_landing_commit': None,
398 'rhodecode_user': {
398 'rhodecode_user': {
399 'username': None,
399 'username': None,
400 'email': None,
400 'email': None,
401 'notification_status': False
401 'notification_status': False
402 },
402 },
403 'visual': {
403 'visual': {
404 'default_renderer': None
404 'default_renderer': None
405 },
405 },
406 'commit_data': {
406 'commit_data': {
407 'commit_id': None
407 'commit_id': None
408 },
408 },
409 'pull_request_data': {'pull_request_id': None},
409 'pull_request_data': {'pull_request_id': None},
410 'timeago': {
410 'timeago': {
411 'refresh_time': 120 * 1000,
411 'refresh_time': 120 * 1000,
412 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
412 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
413 },
413 },
414 'pyramid_dispatch': {
414 'pyramid_dispatch': {
415
415
416 },
416 },
417 'extra': {'plugins': {}}
417 'extra': {'plugins': {}}
418 }
418 }
419 # END CONFIG VARS
419 # END CONFIG VARS
420
420
421 # TODO: This dosn't work when called from pylons compatibility tween.
421 # TODO: This dosn't work when called from pylons compatibility tween.
422 # Fix this and remove it from base controller.
422 # Fix this and remove it from base controller.
423 # context.repo_name = get_repo_slug(request) # can be empty
423 # context.repo_name = get_repo_slug(request) # can be empty
424
424
425 diffmode = 'sideside'
425 diffmode = 'sideside'
426 if request.GET.get('diffmode'):
426 if request.GET.get('diffmode'):
427 if request.GET['diffmode'] == 'unified':
427 if request.GET['diffmode'] == 'unified':
428 diffmode = 'unified'
428 diffmode = 'unified'
429 elif request.session.get('diffmode'):
429 elif request.session.get('diffmode'):
430 diffmode = request.session['diffmode']
430 diffmode = request.session['diffmode']
431
431
432 context.diffmode = diffmode
432 context.diffmode = diffmode
433
433
434 if request.session.get('diffmode') != diffmode:
434 if request.session.get('diffmode') != diffmode:
435 request.session['diffmode'] = diffmode
435 request.session['diffmode'] = diffmode
436
436
437 context.csrf_token = auth.get_csrf_token(session=request.session)
437 context.csrf_token = auth.get_csrf_token(session=request.session)
438 context.backends = rhodecode.BACKENDS.keys()
438 context.backends = rhodecode.BACKENDS.keys()
439 context.backends.sort()
439 context.backends.sort()
440 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
441
441
442 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
443 # given request will ALWAYS be pyramid one
443 # given request will ALWAYS be pyramid one
444 pyramid_request = pyramid.threadlocal.get_current_request()
444 pyramid_request = pyramid.threadlocal.get_current_request()
445 context.pyramid_request = pyramid_request
445 context.pyramid_request = pyramid_request
446
446
447 # web case
447 # web case
448 if hasattr(pyramid_request, 'user'):
448 if hasattr(pyramid_request, 'user'):
449 context.auth_user = pyramid_request.user
449 context.auth_user = pyramid_request.user
450 context.rhodecode_user = pyramid_request.user
450 context.rhodecode_user = pyramid_request.user
451
451
452 # api case
452 # api case
453 if hasattr(pyramid_request, 'rpc_user'):
453 if hasattr(pyramid_request, 'rpc_user'):
454 context.auth_user = pyramid_request.rpc_user
454 context.auth_user = pyramid_request.rpc_user
455 context.rhodecode_user = pyramid_request.rpc_user
455 context.rhodecode_user = pyramid_request.rpc_user
456
456
457 # attach the whole call context to the request
457 # attach the whole call context to the request
458 request.call_context = context
458 request.call_context = context
459
459
460
460
461 def get_auth_user(request):
461 def get_auth_user(request):
462 environ = request.environ
462 environ = request.environ
463 session = request.session
463 session = request.session
464
464
465 ip_addr = get_ip_addr(environ)
465 ip_addr = get_ip_addr(environ)
466 # make sure that we update permissions each time we call controller
466 # make sure that we update permissions each time we call controller
467 _auth_token = (request.GET.get('auth_token', '') or
467 _auth_token = (request.GET.get('auth_token', '') or
468 request.GET.get('api_key', ''))
468 request.GET.get('api_key', ''))
469
469
470 if _auth_token:
470 if _auth_token:
471 # when using API_KEY we assume user exists, and
471 # when using API_KEY we assume user exists, and
472 # doesn't need auth based on cookies.
472 # doesn't need auth based on cookies.
473 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
474 authenticated = False
474 authenticated = False
475 else:
475 else:
476 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
477 try:
477 try:
478 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
479 ip_addr=ip_addr)
479 ip_addr=ip_addr)
480 except UserCreationError as e:
480 except UserCreationError as e:
481 h.flash(e, 'error')
481 h.flash(e, 'error')
482 # container auth or other auth functions that create users
482 # container auth or other auth functions that create users
483 # on the fly can throw this exception signaling that there's
483 # on the fly can throw this exception signaling that there's
484 # issue with user creation, explanation should be provided
484 # issue with user creation, explanation should be provided
485 # in Exception itself. We then create a simple blank
485 # in Exception itself. We then create a simple blank
486 # AuthUser
486 # AuthUser
487 auth_user = AuthUser(ip_addr=ip_addr)
487 auth_user = AuthUser(ip_addr=ip_addr)
488
488
489 if password_changed(auth_user, session):
489 if password_changed(auth_user, session):
490 session.invalidate()
490 session.invalidate()
491 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
492 auth_user = AuthUser(ip_addr=ip_addr)
492 auth_user = AuthUser(ip_addr=ip_addr)
493
493
494 authenticated = cookie_store.get('is_authenticated')
494 authenticated = cookie_store.get('is_authenticated')
495
495
496 if not auth_user.is_authenticated and auth_user.is_user_object:
496 if not auth_user.is_authenticated and auth_user.is_user_object:
497 # user is not authenticated and not empty
497 # user is not authenticated and not empty
498 auth_user.set_authenticated(authenticated)
498 auth_user.set_authenticated(authenticated)
499
499
500 return auth_user
500 return auth_user
501
501
502
502
503 class BaseController(WSGIController):
503 class BaseController(WSGIController):
504
504
505 def __before__(self):
505 def __before__(self):
506 """
506 """
507 __before__ is called before controller methods and after __call__
507 __before__ is called before controller methods and after __call__
508 """
508 """
509 # on each call propagate settings calls into global settings.
509 # on each call propagate settings calls into global settings.
510 from pylons import config
510 from pylons import config
511 from pylons import tmpl_context as c, request, url
511 from pylons import tmpl_context as c, request, url
512 set_rhodecode_config(config)
512 set_rhodecode_config(config)
513 attach_context_attributes(c, request, self._rhodecode_user.user_id)
513 attach_context_attributes(c, request, self._rhodecode_user.user_id)
514
514
515 # TODO: Remove this when fixed in attach_context_attributes()
515 # TODO: Remove this when fixed in attach_context_attributes()
516 c.repo_name = get_repo_slug(request) # can be empty
516 c.repo_name = get_repo_slug(request) # can be empty
517
517
518 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
518 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
519 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
519 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
520 self.sa = meta.Session
520 self.sa = meta.Session
521 self.scm_model = ScmModel(self.sa)
521 self.scm_model = ScmModel(self.sa)
522
522
523 # set user language
523 # set user language
524 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
524 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
525 if user_lang:
525 if user_lang:
526 translation.set_lang(user_lang)
526 translation.set_lang(user_lang)
527 log.debug('set language to %s for user %s',
527 log.debug('set language to %s for user %s',
528 user_lang, self._rhodecode_user)
528 user_lang, self._rhodecode_user)
529
529
530 def _dispatch_redirect(self, with_url, environ, start_response):
530 def _dispatch_redirect(self, with_url, environ, start_response):
531 from webob.exc import HTTPFound
531 from webob.exc import HTTPFound
532 resp = HTTPFound(with_url)
532 resp = HTTPFound(with_url)
533 environ['SCRIPT_NAME'] = '' # handle prefix middleware
533 environ['SCRIPT_NAME'] = '' # handle prefix middleware
534 environ['PATH_INFO'] = with_url
534 environ['PATH_INFO'] = with_url
535 return resp(environ, start_response)
535 return resp(environ, start_response)
536
536
537 def __call__(self, environ, start_response):
537 def __call__(self, environ, start_response):
538 """Invoke the Controller"""
538 """Invoke the Controller"""
539 # WSGIController.__call__ dispatches to the Controller method
539 # WSGIController.__call__ dispatches to the Controller method
540 # the request is routed to. This routing information is
540 # the request is routed to. This routing information is
541 # available in environ['pylons.routes_dict']
541 # available in environ['pylons.routes_dict']
542 from rhodecode.lib import helpers as h
542 from rhodecode.lib import helpers as h
543 from pylons import tmpl_context as c, request, url
543 from pylons import tmpl_context as c, request, url
544
544
545 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
545 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
546 if environ.get('debugtoolbar.wants_pylons_context', False):
546 if environ.get('debugtoolbar.wants_pylons_context', False):
547 environ['debugtoolbar.pylons_context'] = c._current_obj()
547 environ['debugtoolbar.pylons_context'] = c._current_obj()
548
548
549 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
549 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
550 environ['pylons.routes_dict']['action']])
550 environ['pylons.routes_dict']['action']])
551
551
552 self.rc_config = SettingsModel().get_all_settings(cache=True)
552 self.rc_config = SettingsModel().get_all_settings(cache=True)
553 self.ip_addr = get_ip_addr(environ)
553 self.ip_addr = get_ip_addr(environ)
554
554
555 # The rhodecode auth user is looked up and passed through the
555 # The rhodecode auth user is looked up and passed through the
556 # environ by the pylons compatibility tween in pyramid.
556 # environ by the pylons compatibility tween in pyramid.
557 # So we can just grab it from there.
557 # So we can just grab it from there.
558 auth_user = environ['rc_auth_user']
558 auth_user = environ['rc_auth_user']
559
559
560 # set globals for auth user
560 # set globals for auth user
561 request.user = auth_user
561 request.user = auth_user
562 self._rhodecode_user = auth_user
562 self._rhodecode_user = auth_user
563
563
564 log.info('IP: %s User: %s accessed %s [%s]' % (
564 log.info('IP: %s User: %s accessed %s [%s]' % (
565 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
565 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
566 _route_name)
566 _route_name)
567 )
567 )
568
568
569 user_obj = auth_user.get_instance()
569 user_obj = auth_user.get_instance()
570 if user_obj and user_obj.user_data.get('force_password_change'):
570 if user_obj and user_obj.user_data.get('force_password_change'):
571 h.flash('You are required to change your password', 'warning',
571 h.flash('You are required to change your password', 'warning',
572 ignore_duplicate=True)
572 ignore_duplicate=True)
573 return self._dispatch_redirect(
573 return self._dispatch_redirect(
574 url('my_account_password'), environ, start_response)
574 url('my_account_password'), environ, start_response)
575
575
576 return WSGIController.__call__(self, environ, start_response)
576 return WSGIController.__call__(self, environ, start_response)
577
577
578
578
579 def h_filter(s):
579 def h_filter(s):
580 """
580 """
581 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
581 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
582 we wrap this with additional functionality that converts None to empty
582 we wrap this with additional functionality that converts None to empty
583 strings
583 strings
584 """
584 """
585 if s is None:
585 if s is None:
586 return markupsafe.Markup()
586 return markupsafe.Markup()
587 return markupsafe.escape(s)
587 return markupsafe.escape(s)
588
588
589
589
590 def add_events_routes(config):
590 def add_events_routes(config):
591 """
591 """
592 Adds routing that can be used in events. Because some events are triggered
592 Adds routing that can be used in events. Because some events are triggered
593 outside of pyramid context, we need to bootstrap request with some
593 outside of pyramid context, we need to bootstrap request with some
594 routing registered
594 routing registered
595 """
595 """
596 config.add_route(name='home', pattern='/')
596 config.add_route(name='home', pattern='/')
597
597
598 config.add_route(name='repo_summary', pattern='/{repo_name}')
598 config.add_route(name='repo_summary', pattern='/{repo_name}')
599 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
599 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
600 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
600 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
601
601
602 config.add_route(name='pullrequest_show',
602 config.add_route(name='pullrequest_show',
603 pattern='/{repo_name}/pull-request/{pull_request_id}')
603 pattern='/{repo_name}/pull-request/{pull_request_id}')
604 config.add_route(name='pull_requests_global',
604 config.add_route(name='pull_requests_global',
605 pattern='/pull-request/{pull_request_id}')
605 pattern='/pull-request/{pull_request_id}')
606
606
607 config.add_route(name='repo_commit',
607 config.add_route(name='repo_commit',
608 pattern='/{repo_name}/changeset/{commit_id}')
608 pattern='/{repo_name}/changeset/{commit_id}')
609 config.add_route(name='repo_files',
609 config.add_route(name='repo_files',
610 pattern='/{repo_name}/files/{commit_id}/{f_path}')
610 pattern='/{repo_name}/files/{commit_id}/{f_path}')
611
611
612
612
613 def bootstrap_request(**kwargs):
613 def bootstrap_request(**kwargs):
614 import pyramid.testing
614 import pyramid.testing
615
615
616 class TestRequest(pyramid.testing.DummyRequest):
616 class TestRequest(pyramid.testing.DummyRequest):
617 application_url = kwargs.pop('application_url', 'http://example.com')
617 application_url = kwargs.pop('application_url', 'http://example.com')
618 host = kwargs.pop('host', 'example.com:80')
618 host = kwargs.pop('host', 'example.com:80')
619 domain = kwargs.pop('domain', 'example.com')
619 domain = kwargs.pop('domain', 'example.com')
620
620
621 def translate(self, msg):
622 return msg
623
621 class TestDummySession(pyramid.testing.DummySession):
624 class TestDummySession(pyramid.testing.DummySession):
622 def save(*arg, **kw):
625 def save(*arg, **kw):
623 pass
626 pass
624
627
625 request = TestRequest(**kwargs)
628 request = TestRequest(**kwargs)
626 request.session = TestDummySession()
629 request.session = TestDummySession()
627
630
631
628 config = pyramid.testing.setUp(request=request)
632 config = pyramid.testing.setUp(request=request)
629 add_events_routes(config)
633 add_events_routes(config)
630 return request
634 return request
631
635
@@ -1,1611 +1,1622 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 from collections import namedtuple
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
32
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
36
34
37 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
44 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
47 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
48 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
49 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
52 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
54 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
57 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
59 from rhodecode.model.settings import VcsSettingsModel
61
60
62
61
63 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
64
63
65
64
66 # 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
67 # request update.
66 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
71
70
72
71
73 class PullRequestModel(BaseModel):
72 class PullRequestModel(BaseModel):
74
73
75 cls = PullRequest
74 cls = PullRequest
76
75
77 DIFF_CONTEXT = 3
76 DIFF_CONTEXT = 3
78
77
79 MERGE_STATUS_MESSAGES = {
78 MERGE_STATUS_MESSAGES = {
80 MergeFailureReason.NONE: lazy_ugettext(
79 MergeFailureReason.NONE: lazy_ugettext(
81 'This pull request can be automatically merged.'),
80 'This pull request can be automatically merged.'),
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 'This pull request cannot be merged because of an unhandled'
82 'This pull request cannot be merged because of an unhandled'
84 ' exception.'),
83 ' exception.'),
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 'This pull request cannot be merged because of merge conflicts.'),
85 'This pull request cannot be merged because of merge conflicts.'),
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 'This pull request could not be merged because push to target'
87 'This pull request could not be merged because push to target'
89 ' failed.'),
88 ' failed.'),
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 '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'
92 ' head.'),
91 ' head.'),
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 'This pull request cannot be merged because the source contains'
93 'This pull request cannot be merged because the source contains'
95 ' more branches than the target.'),
94 ' more branches than the target.'),
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 'This pull request cannot be merged because the target has'
96 'This pull request cannot be merged because the target has'
98 ' multiple heads.'),
97 ' multiple heads.'),
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 'This pull request cannot be merged because the target repository'
99 'This pull request cannot be merged because the target repository'
101 ' is locked.'),
100 ' is locked.'),
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 'This pull request cannot be merged because the target or the '
102 'This pull request cannot be merged because the target or the '
104 'source reference is missing.'),
103 'source reference is missing.'),
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 'This pull request cannot be merged because the target '
105 'This pull request cannot be merged because the target '
107 'reference is missing.'),
106 'reference is missing.'),
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 'This pull request cannot be merged because the source '
108 'This pull request cannot be merged because the source '
110 'reference is missing.'),
109 'reference is missing.'),
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 'This pull request cannot be merged because of conflicts related '
111 'This pull request cannot be merged because of conflicts related '
113 'to sub repositories.'),
112 'to sub repositories.'),
114 }
113 }
115
114
116 UPDATE_STATUS_MESSAGES = {
115 UPDATE_STATUS_MESSAGES = {
117 UpdateFailureReason.NONE: lazy_ugettext(
116 UpdateFailureReason.NONE: lazy_ugettext(
118 'Pull request update successful.'),
117 'Pull request update successful.'),
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 'Pull request update failed because of an unknown error.'),
119 'Pull request update failed because of an unknown error.'),
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 'No update needed because the source and target have not changed.'),
121 'No update needed because the source and target have not changed.'),
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 'Pull request cannot be updated because the reference type is '
123 'Pull request cannot be updated because the reference type is '
125 '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.'),
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 'This pull request cannot be updated because the target '
126 'This pull request cannot be updated because the target '
128 'reference is missing.'),
127 'reference is missing.'),
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 'This pull request cannot be updated because the source '
129 'This pull request cannot be updated because the source '
131 'reference is missing.'),
130 'reference is missing.'),
132 }
131 }
133
132
134 def __get_pull_request(self, pull_request):
133 def __get_pull_request(self, pull_request):
135 return self._get_instance((
134 return self._get_instance((
136 PullRequest, PullRequestVersion), pull_request)
135 PullRequest, PullRequestVersion), pull_request)
137
136
138 def _check_perms(self, perms, pull_request, user, api=False):
137 def _check_perms(self, perms, pull_request, user, api=False):
139 if not api:
138 if not api:
140 return h.HasRepoPermissionAny(*perms)(
139 return h.HasRepoPermissionAny(*perms)(
141 user=user, repo_name=pull_request.target_repo.repo_name)
140 user=user, repo_name=pull_request.target_repo.repo_name)
142 else:
141 else:
143 return h.HasRepoPermissionAnyApi(*perms)(
142 return h.HasRepoPermissionAnyApi(*perms)(
144 user=user, repo_name=pull_request.target_repo.repo_name)
143 user=user, repo_name=pull_request.target_repo.repo_name)
145
144
146 def check_user_read(self, pull_request, user, api=False):
145 def check_user_read(self, pull_request, user, api=False):
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 return self._check_perms(_perms, pull_request, user, api)
147 return self._check_perms(_perms, pull_request, user, api)
149
148
150 def check_user_merge(self, pull_request, user, api=False):
149 def check_user_merge(self, pull_request, user, api=False):
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 return self._check_perms(_perms, pull_request, user, api)
151 return self._check_perms(_perms, pull_request, user, api)
153
152
154 def check_user_update(self, pull_request, user, api=False):
153 def check_user_update(self, pull_request, user, api=False):
155 owner = user.user_id == pull_request.user_id
154 owner = user.user_id == pull_request.user_id
156 return self.check_user_merge(pull_request, user, api) or owner
155 return self.check_user_merge(pull_request, user, api) or owner
157
156
158 def check_user_delete(self, pull_request, user):
157 def check_user_delete(self, pull_request, user):
159 owner = user.user_id == pull_request.user_id
158 owner = user.user_id == pull_request.user_id
160 _perms = ('repository.admin',)
159 _perms = ('repository.admin',)
161 return self._check_perms(_perms, pull_request, user) or owner
160 return self._check_perms(_perms, pull_request, user) or owner
162
161
163 def check_user_change_status(self, pull_request, user, api=False):
162 def check_user_change_status(self, pull_request, user, api=False):
164 reviewer = user.user_id in [x.user_id for x in
163 reviewer = user.user_id in [x.user_id for x in
165 pull_request.reviewers]
164 pull_request.reviewers]
166 return self.check_user_update(pull_request, user, api) or reviewer
165 return self.check_user_update(pull_request, user, api) or reviewer
167
166
168 def get(self, pull_request):
167 def get(self, pull_request):
169 return self.__get_pull_request(pull_request)
168 return self.__get_pull_request(pull_request)
170
169
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 opened_by=None, order_by=None,
171 opened_by=None, order_by=None,
173 order_dir='desc'):
172 order_dir='desc'):
174 repo = None
173 repo = None
175 if repo_name:
174 if repo_name:
176 repo = self._get_repo(repo_name)
175 repo = self._get_repo(repo_name)
177
176
178 q = PullRequest.query()
177 q = PullRequest.query()
179
178
180 # source or target
179 # source or target
181 if repo and source:
180 if repo and source:
182 q = q.filter(PullRequest.source_repo == repo)
181 q = q.filter(PullRequest.source_repo == repo)
183 elif repo:
182 elif repo:
184 q = q.filter(PullRequest.target_repo == repo)
183 q = q.filter(PullRequest.target_repo == repo)
185
184
186 # closed,opened
185 # closed,opened
187 if statuses:
186 if statuses:
188 q = q.filter(PullRequest.status.in_(statuses))
187 q = q.filter(PullRequest.status.in_(statuses))
189
188
190 # opened by filter
189 # opened by filter
191 if opened_by:
190 if opened_by:
192 q = q.filter(PullRequest.user_id.in_(opened_by))
191 q = q.filter(PullRequest.user_id.in_(opened_by))
193
192
194 if order_by:
193 if order_by:
195 order_map = {
194 order_map = {
196 'name_raw': PullRequest.pull_request_id,
195 'name_raw': PullRequest.pull_request_id,
197 'title': PullRequest.title,
196 'title': PullRequest.title,
198 'updated_on_raw': PullRequest.updated_on,
197 'updated_on_raw': PullRequest.updated_on,
199 'target_repo': PullRequest.target_repo_id
198 'target_repo': PullRequest.target_repo_id
200 }
199 }
201 if order_dir == 'asc':
200 if order_dir == 'asc':
202 q = q.order_by(order_map[order_by].asc())
201 q = q.order_by(order_map[order_by].asc())
203 else:
202 else:
204 q = q.order_by(order_map[order_by].desc())
203 q = q.order_by(order_map[order_by].desc())
205
204
206 return q
205 return q
207
206
208 def count_all(self, repo_name, source=False, statuses=None,
207 def count_all(self, repo_name, source=False, statuses=None,
209 opened_by=None):
208 opened_by=None):
210 """
209 """
211 Count the number of pull requests for a specific repository.
210 Count the number of pull requests for a specific repository.
212
211
213 :param repo_name: target or source repo
212 :param repo_name: target or source repo
214 :param source: boolean flag to specify if repo_name refers to source
213 :param source: boolean flag to specify if repo_name refers to source
215 :param statuses: list of pull request statuses
214 :param statuses: list of pull request statuses
216 :param opened_by: author user of the pull request
215 :param opened_by: author user of the pull request
217 :returns: int number of pull requests
216 :returns: int number of pull requests
218 """
217 """
219 q = self._prepare_get_all_query(
218 q = self._prepare_get_all_query(
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221
220
222 return q.count()
221 return q.count()
223
222
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 offset=0, length=None, order_by=None, order_dir='desc'):
224 offset=0, length=None, order_by=None, order_dir='desc'):
226 """
225 """
227 Get all pull requests for a specific repository.
226 Get all pull requests for a specific repository.
228
227
229 :param repo_name: target or source repo
228 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
229 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
230 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
231 :param opened_by: author user of the pull request
233 :param offset: pagination offset
232 :param offset: pagination offset
234 :param length: length of returned list
233 :param length: length of returned list
235 :param order_by: order of the returned list
234 :param order_by: order of the returned list
236 :param order_dir: 'asc' or 'desc' ordering direction
235 :param order_dir: 'asc' or 'desc' ordering direction
237 :returns: list of pull requests
236 :returns: list of pull requests
238 """
237 """
239 q = self._prepare_get_all_query(
238 q = self._prepare_get_all_query(
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 order_by=order_by, order_dir=order_dir)
240 order_by=order_by, order_dir=order_dir)
242
241
243 if length:
242 if length:
244 pull_requests = q.limit(length).offset(offset).all()
243 pull_requests = q.limit(length).offset(offset).all()
245 else:
244 else:
246 pull_requests = q.all()
245 pull_requests = q.all()
247
246
248 return pull_requests
247 return pull_requests
249
248
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None):
250 opened_by=None):
252 """
251 """
253 Count the number of pull requests for a specific repository that are
252 Count the number of pull requests for a specific repository that are
254 awaiting review.
253 awaiting review.
255
254
256 :param repo_name: target or source repo
255 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
256 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
257 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
258 :param opened_by: author user of the pull request
260 :returns: int number of pull requests
259 :returns: int number of pull requests
261 """
260 """
262 pull_requests = self.get_awaiting_review(
261 pull_requests = self.get_awaiting_review(
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264
263
265 return len(pull_requests)
264 return len(pull_requests)
266
265
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 opened_by=None, offset=0, length=None,
267 opened_by=None, offset=0, length=None,
269 order_by=None, order_dir='desc'):
268 order_by=None, order_dir='desc'):
270 """
269 """
271 Get all pull requests for a specific repository that are awaiting
270 Get all pull requests for a specific repository that are awaiting
272 review.
271 review.
273
272
274 :param repo_name: target or source repo
273 :param repo_name: target or source repo
275 :param source: boolean flag to specify if repo_name refers to source
274 :param source: boolean flag to specify if repo_name refers to source
276 :param statuses: list of pull request statuses
275 :param statuses: list of pull request statuses
277 :param opened_by: author user of the pull request
276 :param opened_by: author user of the pull request
278 :param offset: pagination offset
277 :param offset: pagination offset
279 :param length: length of returned list
278 :param length: length of returned list
280 :param order_by: order of the returned list
279 :param order_by: order of the returned list
281 :param order_dir: 'asc' or 'desc' ordering direction
280 :param order_dir: 'asc' or 'desc' ordering direction
282 :returns: list of pull requests
281 :returns: list of pull requests
283 """
282 """
284 pull_requests = self.get_all(
283 pull_requests = self.get_all(
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 order_by=order_by, order_dir=order_dir)
285 order_by=order_by, order_dir=order_dir)
287
286
288 _filtered_pull_requests = []
287 _filtered_pull_requests = []
289 for pr in pull_requests:
288 for pr in pull_requests:
290 status = pr.calculated_review_status()
289 status = pr.calculated_review_status()
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 _filtered_pull_requests.append(pr)
292 _filtered_pull_requests.append(pr)
294 if length:
293 if length:
295 return _filtered_pull_requests[offset:offset+length]
294 return _filtered_pull_requests[offset:offset+length]
296 else:
295 else:
297 return _filtered_pull_requests
296 return _filtered_pull_requests
298
297
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 opened_by=None, user_id=None):
299 opened_by=None, user_id=None):
301 """
300 """
302 Count the number of pull requests for a specific repository that are
301 Count the number of pull requests for a specific repository that are
303 awaiting review from a specific user.
302 awaiting review from a specific user.
304
303
305 :param repo_name: target or source repo
304 :param repo_name: target or source repo
306 :param source: boolean flag to specify if repo_name refers to source
305 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
306 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
307 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
308 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
309 :returns: int number of pull requests
311 """
310 """
312 pull_requests = self.get_awaiting_my_review(
311 pull_requests = self.get_awaiting_my_review(
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 user_id=user_id)
313 user_id=user_id)
315
314
316 return len(pull_requests)
315 return len(pull_requests)
317
316
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
318 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
319 length=None, order_by=None, order_dir='desc'):
321 """
320 """
322 Get all pull requests for a specific repository that are awaiting
321 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
322 review from a specific user.
324
323
325 :param repo_name: target or source repo
324 :param repo_name: target or source repo
326 :param source: boolean flag to specify if repo_name refers to source
325 :param source: boolean flag to specify if repo_name refers to source
327 :param statuses: list of pull request statuses
326 :param statuses: list of pull request statuses
328 :param opened_by: author user of the pull request
327 :param opened_by: author user of the pull request
329 :param user_id: reviewer user of the pull request
328 :param user_id: reviewer user of the pull request
330 :param offset: pagination offset
329 :param offset: pagination offset
331 :param length: length of returned list
330 :param length: length of returned list
332 :param order_by: order of the returned list
331 :param order_by: order of the returned list
333 :param order_dir: 'asc' or 'desc' ordering direction
332 :param order_dir: 'asc' or 'desc' ordering direction
334 :returns: list of pull requests
333 :returns: list of pull requests
335 """
334 """
336 pull_requests = self.get_all(
335 pull_requests = self.get_all(
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 order_by=order_by, order_dir=order_dir)
337 order_by=order_by, order_dir=order_dir)
339
338
340 _my = PullRequestModel().get_not_reviewed(user_id)
339 _my = PullRequestModel().get_not_reviewed(user_id)
341 my_participation = []
340 my_participation = []
342 for pr in pull_requests:
341 for pr in pull_requests:
343 if pr in _my:
342 if pr in _my:
344 my_participation.append(pr)
343 my_participation.append(pr)
345 _filtered_pull_requests = my_participation
344 _filtered_pull_requests = my_participation
346 if length:
345 if length:
347 return _filtered_pull_requests[offset:offset+length]
346 return _filtered_pull_requests[offset:offset+length]
348 else:
347 else:
349 return _filtered_pull_requests
348 return _filtered_pull_requests
350
349
351 def get_not_reviewed(self, user_id):
350 def get_not_reviewed(self, user_id):
352 return [
351 return [
353 x.pull_request for x in PullRequestReviewers.query().filter(
352 x.pull_request for x in PullRequestReviewers.query().filter(
354 PullRequestReviewers.user_id == user_id).all()
353 PullRequestReviewers.user_id == user_id).all()
355 ]
354 ]
356
355
357 def _prepare_participating_query(self, user_id=None, statuses=None,
356 def _prepare_participating_query(self, user_id=None, statuses=None,
358 order_by=None, order_dir='desc'):
357 order_by=None, order_dir='desc'):
359 q = PullRequest.query()
358 q = PullRequest.query()
360 if user_id:
359 if user_id:
361 reviewers_subquery = Session().query(
360 reviewers_subquery = Session().query(
362 PullRequestReviewers.pull_request_id).filter(
361 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.user_id == user_id).subquery()
362 PullRequestReviewers.user_id == user_id).subquery()
364 user_filter= or_(
363 user_filter = or_(
365 PullRequest.user_id == user_id,
364 PullRequest.user_id == user_id,
366 PullRequest.pull_request_id.in_(reviewers_subquery)
365 PullRequest.pull_request_id.in_(reviewers_subquery)
367 )
366 )
368 q = PullRequest.query().filter(user_filter)
367 q = PullRequest.query().filter(user_filter)
369
368
370 # closed,opened
369 # closed,opened
371 if statuses:
370 if statuses:
372 q = q.filter(PullRequest.status.in_(statuses))
371 q = q.filter(PullRequest.status.in_(statuses))
373
372
374 if order_by:
373 if order_by:
375 order_map = {
374 order_map = {
376 'name_raw': PullRequest.pull_request_id,
375 'name_raw': PullRequest.pull_request_id,
377 'title': PullRequest.title,
376 'title': PullRequest.title,
378 'updated_on_raw': PullRequest.updated_on,
377 'updated_on_raw': PullRequest.updated_on,
379 'target_repo': PullRequest.target_repo_id
378 'target_repo': PullRequest.target_repo_id
380 }
379 }
381 if order_dir == 'asc':
380 if order_dir == 'asc':
382 q = q.order_by(order_map[order_by].asc())
381 q = q.order_by(order_map[order_by].asc())
383 else:
382 else:
384 q = q.order_by(order_map[order_by].desc())
383 q = q.order_by(order_map[order_by].desc())
385
384
386 return q
385 return q
387
386
388 def count_im_participating_in(self, user_id=None, statuses=None):
387 def count_im_participating_in(self, user_id=None, statuses=None):
389 q = self._prepare_participating_query(user_id, statuses=statuses)
388 q = self._prepare_participating_query(user_id, statuses=statuses)
390 return q.count()
389 return q.count()
391
390
392 def get_im_participating_in(
391 def get_im_participating_in(
393 self, user_id=None, statuses=None, offset=0,
392 self, user_id=None, statuses=None, offset=0,
394 length=None, order_by=None, order_dir='desc'):
393 length=None, order_by=None, order_dir='desc'):
395 """
394 """
396 Get all Pull requests that i'm participating in, or i have opened
395 Get all Pull requests that i'm participating in, or i have opened
397 """
396 """
398
397
399 q = self._prepare_participating_query(
398 q = self._prepare_participating_query(
400 user_id, statuses=statuses, order_by=order_by,
399 user_id, statuses=statuses, order_by=order_by,
401 order_dir=order_dir)
400 order_dir=order_dir)
402
401
403 if length:
402 if length:
404 pull_requests = q.limit(length).offset(offset).all()
403 pull_requests = q.limit(length).offset(offset).all()
405 else:
404 else:
406 pull_requests = q.all()
405 pull_requests = q.all()
407
406
408 return pull_requests
407 return pull_requests
409
408
410 def get_versions(self, pull_request):
409 def get_versions(self, pull_request):
411 """
410 """
412 returns version of pull request sorted by ID descending
411 returns version of pull request sorted by ID descending
413 """
412 """
414 return PullRequestVersion.query()\
413 return PullRequestVersion.query()\
415 .filter(PullRequestVersion.pull_request == pull_request)\
414 .filter(PullRequestVersion.pull_request == pull_request)\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 .all()
416 .all()
418
417
419 def create(self, created_by, source_repo, source_ref, target_repo,
418 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
419 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
420 reviewer_data=None, translator=None):
421 translator = translator or get_current_request().translate
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437
437
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458 Session().add(reviewer)
458 Session().add(reviewer)
459
459
460 # Set approval status to "Under Review" for all commits which are
460 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
461 # part of this pull request.
462 ChangesetStatusModel().set_status(
462 ChangesetStatusModel().set_status(
463 repo=target_repo,
463 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
465 user=created_by_user,
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 MergeCheck.validate(
470 pull_request, user=created_by_user, translator=translator)
471
469 self.notify_reviewers(pull_request, reviewer_ids)
472 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
473 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
474 pull_request, created_by_user, 'create')
472
475
473 creation_data = pull_request.get_api_data(with_merge_state=False)
476 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
477 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
478 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
479 created_by_user, pull_request)
477
480
478 return pull_request
481 return pull_request
479
482
480 def _trigger_pull_request_hook(self, pull_request, user, action):
483 def _trigger_pull_request_hook(self, pull_request, user, action):
481 pull_request = self.__get_pull_request(pull_request)
484 pull_request = self.__get_pull_request(pull_request)
482 target_scm = pull_request.target_repo.scm_instance()
485 target_scm = pull_request.target_repo.scm_instance()
483 if action == 'create':
486 if action == 'create':
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
487 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 elif action == 'merge':
488 elif action == 'merge':
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
489 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 elif action == 'close':
490 elif action == 'close':
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
491 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 elif action == 'review_status_change':
492 elif action == 'review_status_change':
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
493 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 elif action == 'update':
494 elif action == 'update':
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
495 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 else:
496 else:
494 return
497 return
495
498
496 trigger_hook(
499 trigger_hook(
497 username=user.username,
500 username=user.username,
498 repo_name=pull_request.target_repo.repo_name,
501 repo_name=pull_request.target_repo.repo_name,
499 repo_alias=target_scm.alias,
502 repo_alias=target_scm.alias,
500 pull_request=pull_request)
503 pull_request=pull_request)
501
504
502 def _get_commit_ids(self, pull_request):
505 def _get_commit_ids(self, pull_request):
503 """
506 """
504 Return the commit ids of the merged pull request.
507 Return the commit ids of the merged pull request.
505
508
506 This method is not dealing correctly yet with the lack of autoupdates
509 This method is not dealing correctly yet with the lack of autoupdates
507 nor with the implicit target updates.
510 nor with the implicit target updates.
508 For example: if a commit in the source repo is already in the target it
511 For example: if a commit in the source repo is already in the target it
509 will be reported anyways.
512 will be reported anyways.
510 """
513 """
511 merge_rev = pull_request.merge_rev
514 merge_rev = pull_request.merge_rev
512 if merge_rev is None:
515 if merge_rev is None:
513 raise ValueError('This pull request was not merged yet')
516 raise ValueError('This pull request was not merged yet')
514
517
515 commit_ids = list(pull_request.revisions)
518 commit_ids = list(pull_request.revisions)
516 if merge_rev not in commit_ids:
519 if merge_rev not in commit_ids:
517 commit_ids.append(merge_rev)
520 commit_ids.append(merge_rev)
518
521
519 return commit_ids
522 return commit_ids
520
523
521 def merge(self, pull_request, user, extras):
524 def merge(self, pull_request, user, extras):
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
525 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
526 merge_state = self._merge_pull_request(pull_request, user, extras)
524 if merge_state.executed:
527 if merge_state.executed:
525 log.debug(
528 log.debug(
526 "Merge was successful, updating the pull request comments.")
529 "Merge was successful, updating the pull request comments.")
527 self._comment_and_close_pr(pull_request, user, merge_state)
530 self._comment_and_close_pr(pull_request, user, merge_state)
528
531
529 self._log_audit_action(
532 self._log_audit_action(
530 'repo.pull_request.merge',
533 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
534 {'merge_state': merge_state.__dict__},
532 user, pull_request)
535 user, pull_request)
533
536
534 else:
537 else:
535 log.warn("Merge failed, not updating the pull request.")
538 log.warn("Merge failed, not updating the pull request.")
536 return merge_state
539 return merge_state
537
540
538 def _merge_pull_request(self, pull_request, user, extras):
541 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
539 target_vcs = pull_request.target_repo.scm_instance()
542 target_vcs = pull_request.target_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
543 source_vcs = pull_request.source_repo.scm_instance()
541 target_ref = self._refresh_reference(
544 target_ref = self._refresh_reference(
542 pull_request.target_ref_parts, target_vcs)
545 pull_request.target_ref_parts, target_vcs)
543
546
544 message = _(
547 message = merge_msg or (
545 'Merge pull request #%(pr_id)s from '
548 'Merge pull request #%(pr_id)s from '
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
549 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 'pr_id': pull_request.pull_request_id,
550 'pr_id': pull_request.pull_request_id,
548 'source_repo': source_vcs.name,
551 'source_repo': source_vcs.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
552 'source_ref_name': pull_request.source_ref_parts.name,
550 'pr_title': pull_request.title
553 'pr_title': pull_request.title
551 }
554 }
552
555
553 workspace_id = self._workspace_id(pull_request)
556 workspace_id = self._workspace_id(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
557 use_rebase = self._use_rebase_for_merging(pull_request)
555 close_branch = self._close_branch_before_merging(pull_request)
558 close_branch = self._close_branch_before_merging(pull_request)
556
559
557 callback_daemon, extras = prepare_callback_daemon(
560 callback_daemon, extras = prepare_callback_daemon(
558 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
561 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
559 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
562 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
560
563
561 with callback_daemon:
564 with callback_daemon:
562 # TODO: johbo: Implement a clean way to run a config_override
565 # TODO: johbo: Implement a clean way to run a config_override
563 # for a single call.
566 # for a single call.
564 target_vcs.config.set(
567 target_vcs.config.set(
565 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
568 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
566 merge_state = target_vcs.merge(
569 merge_state = target_vcs.merge(
567 target_ref, source_vcs, pull_request.source_ref_parts,
570 target_ref, source_vcs, pull_request.source_ref_parts,
568 workspace_id, user_name=user.username,
571 workspace_id, user_name=user.username,
569 user_email=user.email, message=message, use_rebase=use_rebase,
572 user_email=user.email, message=message, use_rebase=use_rebase,
570 close_branch=close_branch)
573 close_branch=close_branch)
571 return merge_state
574 return merge_state
572
575
573 def _comment_and_close_pr(self, pull_request, user, merge_state):
576 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
574 pull_request.merge_rev = merge_state.merge_ref.commit_id
577 pull_request.merge_rev = merge_state.merge_ref.commit_id
575 pull_request.updated_on = datetime.datetime.now()
578 pull_request.updated_on = datetime.datetime.now()
579 close_msg = close_msg or 'Pull request merged and closed'
576
580
577 CommentsModel().create(
581 CommentsModel().create(
578 text=unicode(_('Pull request merged and closed')),
582 text=safe_unicode(close_msg),
579 repo=pull_request.target_repo.repo_id,
583 repo=pull_request.target_repo.repo_id,
580 user=user.user_id,
584 user=user.user_id,
581 pull_request=pull_request.pull_request_id,
585 pull_request=pull_request.pull_request_id,
582 f_path=None,
586 f_path=None,
583 line_no=None,
587 line_no=None,
584 closing_pr=True
588 closing_pr=True
585 )
589 )
586
590
587 Session().add(pull_request)
591 Session().add(pull_request)
588 Session().flush()
592 Session().flush()
589 # TODO: paris: replace invalidation with less radical solution
593 # TODO: paris: replace invalidation with less radical solution
590 ScmModel().mark_for_invalidation(
594 ScmModel().mark_for_invalidation(
591 pull_request.target_repo.repo_name)
595 pull_request.target_repo.repo_name)
592 self._trigger_pull_request_hook(pull_request, user, 'merge')
596 self._trigger_pull_request_hook(pull_request, user, 'merge')
593
597
594 def has_valid_update_type(self, pull_request):
598 def has_valid_update_type(self, pull_request):
595 source_ref_type = pull_request.source_ref_parts.type
599 source_ref_type = pull_request.source_ref_parts.type
596 return source_ref_type in ['book', 'branch', 'tag']
600 return source_ref_type in ['book', 'branch', 'tag']
597
601
598 def update_commits(self, pull_request):
602 def update_commits(self, pull_request):
599 """
603 """
600 Get the updated list of commits for the pull request
604 Get the updated list of commits for the pull request
601 and return the new pull request version and the list
605 and return the new pull request version and the list
602 of commits processed by this update action
606 of commits processed by this update action
603 """
607 """
604 pull_request = self.__get_pull_request(pull_request)
608 pull_request = self.__get_pull_request(pull_request)
605 source_ref_type = pull_request.source_ref_parts.type
609 source_ref_type = pull_request.source_ref_parts.type
606 source_ref_name = pull_request.source_ref_parts.name
610 source_ref_name = pull_request.source_ref_parts.name
607 source_ref_id = pull_request.source_ref_parts.commit_id
611 source_ref_id = pull_request.source_ref_parts.commit_id
608
612
609 target_ref_type = pull_request.target_ref_parts.type
613 target_ref_type = pull_request.target_ref_parts.type
610 target_ref_name = pull_request.target_ref_parts.name
614 target_ref_name = pull_request.target_ref_parts.name
611 target_ref_id = pull_request.target_ref_parts.commit_id
615 target_ref_id = pull_request.target_ref_parts.commit_id
612
616
613 if not self.has_valid_update_type(pull_request):
617 if not self.has_valid_update_type(pull_request):
614 log.debug(
618 log.debug(
615 "Skipping update of pull request %s due to ref type: %s",
619 "Skipping update of pull request %s due to ref type: %s",
616 pull_request, source_ref_type)
620 pull_request, source_ref_type)
617 return UpdateResponse(
621 return UpdateResponse(
618 executed=False,
622 executed=False,
619 reason=UpdateFailureReason.WRONG_REF_TYPE,
623 reason=UpdateFailureReason.WRONG_REF_TYPE,
620 old=pull_request, new=None, changes=None,
624 old=pull_request, new=None, changes=None,
621 source_changed=False, target_changed=False)
625 source_changed=False, target_changed=False)
622
626
623 # source repo
627 # source repo
624 source_repo = pull_request.source_repo.scm_instance()
628 source_repo = pull_request.source_repo.scm_instance()
625 try:
629 try:
626 source_commit = source_repo.get_commit(commit_id=source_ref_name)
630 source_commit = source_repo.get_commit(commit_id=source_ref_name)
627 except CommitDoesNotExistError:
631 except CommitDoesNotExistError:
628 return UpdateResponse(
632 return UpdateResponse(
629 executed=False,
633 executed=False,
630 reason=UpdateFailureReason.MISSING_SOURCE_REF,
634 reason=UpdateFailureReason.MISSING_SOURCE_REF,
631 old=pull_request, new=None, changes=None,
635 old=pull_request, new=None, changes=None,
632 source_changed=False, target_changed=False)
636 source_changed=False, target_changed=False)
633
637
634 source_changed = source_ref_id != source_commit.raw_id
638 source_changed = source_ref_id != source_commit.raw_id
635
639
636 # target repo
640 # target repo
637 target_repo = pull_request.target_repo.scm_instance()
641 target_repo = pull_request.target_repo.scm_instance()
638 try:
642 try:
639 target_commit = target_repo.get_commit(commit_id=target_ref_name)
643 target_commit = target_repo.get_commit(commit_id=target_ref_name)
640 except CommitDoesNotExistError:
644 except CommitDoesNotExistError:
641 return UpdateResponse(
645 return UpdateResponse(
642 executed=False,
646 executed=False,
643 reason=UpdateFailureReason.MISSING_TARGET_REF,
647 reason=UpdateFailureReason.MISSING_TARGET_REF,
644 old=pull_request, new=None, changes=None,
648 old=pull_request, new=None, changes=None,
645 source_changed=False, target_changed=False)
649 source_changed=False, target_changed=False)
646 target_changed = target_ref_id != target_commit.raw_id
650 target_changed = target_ref_id != target_commit.raw_id
647
651
648 if not (source_changed or target_changed):
652 if not (source_changed or target_changed):
649 log.debug("Nothing changed in pull request %s", pull_request)
653 log.debug("Nothing changed in pull request %s", pull_request)
650 return UpdateResponse(
654 return UpdateResponse(
651 executed=False,
655 executed=False,
652 reason=UpdateFailureReason.NO_CHANGE,
656 reason=UpdateFailureReason.NO_CHANGE,
653 old=pull_request, new=None, changes=None,
657 old=pull_request, new=None, changes=None,
654 source_changed=target_changed, target_changed=source_changed)
658 source_changed=target_changed, target_changed=source_changed)
655
659
656 change_in_found = 'target repo' if target_changed else 'source repo'
660 change_in_found = 'target repo' if target_changed else 'source repo'
657 log.debug('Updating pull request because of change in %s detected',
661 log.debug('Updating pull request because of change in %s detected',
658 change_in_found)
662 change_in_found)
659
663
660 # Finally there is a need for an update, in case of source change
664 # Finally there is a need for an update, in case of source change
661 # we create a new version, else just an update
665 # we create a new version, else just an update
662 if source_changed:
666 if source_changed:
663 pull_request_version = self._create_version_from_snapshot(pull_request)
667 pull_request_version = self._create_version_from_snapshot(pull_request)
664 self._link_comments_to_version(pull_request_version)
668 self._link_comments_to_version(pull_request_version)
665 else:
669 else:
666 try:
670 try:
667 ver = pull_request.versions[-1]
671 ver = pull_request.versions[-1]
668 except IndexError:
672 except IndexError:
669 ver = None
673 ver = None
670
674
671 pull_request.pull_request_version_id = \
675 pull_request.pull_request_version_id = \
672 ver.pull_request_version_id if ver else None
676 ver.pull_request_version_id if ver else None
673 pull_request_version = pull_request
677 pull_request_version = pull_request
674
678
675 try:
679 try:
676 if target_ref_type in ('tag', 'branch', 'book'):
680 if target_ref_type in ('tag', 'branch', 'book'):
677 target_commit = target_repo.get_commit(target_ref_name)
681 target_commit = target_repo.get_commit(target_ref_name)
678 else:
682 else:
679 target_commit = target_repo.get_commit(target_ref_id)
683 target_commit = target_repo.get_commit(target_ref_id)
680 except CommitDoesNotExistError:
684 except CommitDoesNotExistError:
681 return UpdateResponse(
685 return UpdateResponse(
682 executed=False,
686 executed=False,
683 reason=UpdateFailureReason.MISSING_TARGET_REF,
687 reason=UpdateFailureReason.MISSING_TARGET_REF,
684 old=pull_request, new=None, changes=None,
688 old=pull_request, new=None, changes=None,
685 source_changed=source_changed, target_changed=target_changed)
689 source_changed=source_changed, target_changed=target_changed)
686
690
687 # re-compute commit ids
691 # re-compute commit ids
688 old_commit_ids = pull_request.revisions
692 old_commit_ids = pull_request.revisions
689 pre_load = ["author", "branch", "date", "message"]
693 pre_load = ["author", "branch", "date", "message"]
690 commit_ranges = target_repo.compare(
694 commit_ranges = target_repo.compare(
691 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
695 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
692 pre_load=pre_load)
696 pre_load=pre_load)
693
697
694 ancestor = target_repo.get_common_ancestor(
698 ancestor = target_repo.get_common_ancestor(
695 target_commit.raw_id, source_commit.raw_id, source_repo)
699 target_commit.raw_id, source_commit.raw_id, source_repo)
696
700
697 pull_request.source_ref = '%s:%s:%s' % (
701 pull_request.source_ref = '%s:%s:%s' % (
698 source_ref_type, source_ref_name, source_commit.raw_id)
702 source_ref_type, source_ref_name, source_commit.raw_id)
699 pull_request.target_ref = '%s:%s:%s' % (
703 pull_request.target_ref = '%s:%s:%s' % (
700 target_ref_type, target_ref_name, ancestor)
704 target_ref_type, target_ref_name, ancestor)
701
705
702 pull_request.revisions = [
706 pull_request.revisions = [
703 commit.raw_id for commit in reversed(commit_ranges)]
707 commit.raw_id for commit in reversed(commit_ranges)]
704 pull_request.updated_on = datetime.datetime.now()
708 pull_request.updated_on = datetime.datetime.now()
705 Session().add(pull_request)
709 Session().add(pull_request)
706 new_commit_ids = pull_request.revisions
710 new_commit_ids = pull_request.revisions
707
711
708 old_diff_data, new_diff_data = self._generate_update_diffs(
712 old_diff_data, new_diff_data = self._generate_update_diffs(
709 pull_request, pull_request_version)
713 pull_request, pull_request_version)
710
714
711 # calculate commit and file changes
715 # calculate commit and file changes
712 changes = self._calculate_commit_id_changes(
716 changes = self._calculate_commit_id_changes(
713 old_commit_ids, new_commit_ids)
717 old_commit_ids, new_commit_ids)
714 file_changes = self._calculate_file_changes(
718 file_changes = self._calculate_file_changes(
715 old_diff_data, new_diff_data)
719 old_diff_data, new_diff_data)
716
720
717 # set comments as outdated if DIFFS changed
721 # set comments as outdated if DIFFS changed
718 CommentsModel().outdate_comments(
722 CommentsModel().outdate_comments(
719 pull_request, old_diff_data=old_diff_data,
723 pull_request, old_diff_data=old_diff_data,
720 new_diff_data=new_diff_data)
724 new_diff_data=new_diff_data)
721
725
722 commit_changes = (changes.added or changes.removed)
726 commit_changes = (changes.added or changes.removed)
723 file_node_changes = (
727 file_node_changes = (
724 file_changes.added or file_changes.modified or file_changes.removed)
728 file_changes.added or file_changes.modified or file_changes.removed)
725 pr_has_changes = commit_changes or file_node_changes
729 pr_has_changes = commit_changes or file_node_changes
726
730
727 # Add an automatic comment to the pull request, in case
731 # Add an automatic comment to the pull request, in case
728 # anything has changed
732 # anything has changed
729 if pr_has_changes:
733 if pr_has_changes:
730 update_comment = CommentsModel().create(
734 update_comment = CommentsModel().create(
731 text=self._render_update_message(changes, file_changes),
735 text=self._render_update_message(changes, file_changes),
732 repo=pull_request.target_repo,
736 repo=pull_request.target_repo,
733 user=pull_request.author,
737 user=pull_request.author,
734 pull_request=pull_request,
738 pull_request=pull_request,
735 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
739 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
736
740
737 # Update status to "Under Review" for added commits
741 # Update status to "Under Review" for added commits
738 for commit_id in changes.added:
742 for commit_id in changes.added:
739 ChangesetStatusModel().set_status(
743 ChangesetStatusModel().set_status(
740 repo=pull_request.source_repo,
744 repo=pull_request.source_repo,
741 status=ChangesetStatus.STATUS_UNDER_REVIEW,
745 status=ChangesetStatus.STATUS_UNDER_REVIEW,
742 comment=update_comment,
746 comment=update_comment,
743 user=pull_request.author,
747 user=pull_request.author,
744 pull_request=pull_request,
748 pull_request=pull_request,
745 revision=commit_id)
749 revision=commit_id)
746
750
747 log.debug(
751 log.debug(
748 'Updated pull request %s, added_ids: %s, common_ids: %s, '
752 'Updated pull request %s, added_ids: %s, common_ids: %s, '
749 'removed_ids: %s', pull_request.pull_request_id,
753 'removed_ids: %s', pull_request.pull_request_id,
750 changes.added, changes.common, changes.removed)
754 changes.added, changes.common, changes.removed)
751 log.debug(
755 log.debug(
752 'Updated pull request with the following file changes: %s',
756 'Updated pull request with the following file changes: %s',
753 file_changes)
757 file_changes)
754
758
755 log.info(
759 log.info(
756 "Updated pull request %s from commit %s to commit %s, "
760 "Updated pull request %s from commit %s to commit %s, "
757 "stored new version %s of this pull request.",
761 "stored new version %s of this pull request.",
758 pull_request.pull_request_id, source_ref_id,
762 pull_request.pull_request_id, source_ref_id,
759 pull_request.source_ref_parts.commit_id,
763 pull_request.source_ref_parts.commit_id,
760 pull_request_version.pull_request_version_id)
764 pull_request_version.pull_request_version_id)
761 Session().commit()
765 Session().commit()
762 self._trigger_pull_request_hook(
766 self._trigger_pull_request_hook(
763 pull_request, pull_request.author, 'update')
767 pull_request, pull_request.author, 'update')
764
768
765 return UpdateResponse(
769 return UpdateResponse(
766 executed=True, reason=UpdateFailureReason.NONE,
770 executed=True, reason=UpdateFailureReason.NONE,
767 old=pull_request, new=pull_request_version, changes=changes,
771 old=pull_request, new=pull_request_version, changes=changes,
768 source_changed=source_changed, target_changed=target_changed)
772 source_changed=source_changed, target_changed=target_changed)
769
773
770 def _create_version_from_snapshot(self, pull_request):
774 def _create_version_from_snapshot(self, pull_request):
771 version = PullRequestVersion()
775 version = PullRequestVersion()
772 version.title = pull_request.title
776 version.title = pull_request.title
773 version.description = pull_request.description
777 version.description = pull_request.description
774 version.status = pull_request.status
778 version.status = pull_request.status
775 version.created_on = datetime.datetime.now()
779 version.created_on = datetime.datetime.now()
776 version.updated_on = pull_request.updated_on
780 version.updated_on = pull_request.updated_on
777 version.user_id = pull_request.user_id
781 version.user_id = pull_request.user_id
778 version.source_repo = pull_request.source_repo
782 version.source_repo = pull_request.source_repo
779 version.source_ref = pull_request.source_ref
783 version.source_ref = pull_request.source_ref
780 version.target_repo = pull_request.target_repo
784 version.target_repo = pull_request.target_repo
781 version.target_ref = pull_request.target_ref
785 version.target_ref = pull_request.target_ref
782
786
783 version._last_merge_source_rev = pull_request._last_merge_source_rev
787 version._last_merge_source_rev = pull_request._last_merge_source_rev
784 version._last_merge_target_rev = pull_request._last_merge_target_rev
788 version._last_merge_target_rev = pull_request._last_merge_target_rev
785 version.last_merge_status = pull_request.last_merge_status
789 version.last_merge_status = pull_request.last_merge_status
786 version.shadow_merge_ref = pull_request.shadow_merge_ref
790 version.shadow_merge_ref = pull_request.shadow_merge_ref
787 version.merge_rev = pull_request.merge_rev
791 version.merge_rev = pull_request.merge_rev
788 version.reviewer_data = pull_request.reviewer_data
792 version.reviewer_data = pull_request.reviewer_data
789
793
790 version.revisions = pull_request.revisions
794 version.revisions = pull_request.revisions
791 version.pull_request = pull_request
795 version.pull_request = pull_request
792 Session().add(version)
796 Session().add(version)
793 Session().flush()
797 Session().flush()
794
798
795 return version
799 return version
796
800
797 def _generate_update_diffs(self, pull_request, pull_request_version):
801 def _generate_update_diffs(self, pull_request, pull_request_version):
798
802
799 diff_context = (
803 diff_context = (
800 self.DIFF_CONTEXT +
804 self.DIFF_CONTEXT +
801 CommentsModel.needed_extra_diff_context())
805 CommentsModel.needed_extra_diff_context())
802
806
803 source_repo = pull_request_version.source_repo
807 source_repo = pull_request_version.source_repo
804 source_ref_id = pull_request_version.source_ref_parts.commit_id
808 source_ref_id = pull_request_version.source_ref_parts.commit_id
805 target_ref_id = pull_request_version.target_ref_parts.commit_id
809 target_ref_id = pull_request_version.target_ref_parts.commit_id
806 old_diff = self._get_diff_from_pr_or_version(
810 old_diff = self._get_diff_from_pr_or_version(
807 source_repo, source_ref_id, target_ref_id, context=diff_context)
811 source_repo, source_ref_id, target_ref_id, context=diff_context)
808
812
809 source_repo = pull_request.source_repo
813 source_repo = pull_request.source_repo
810 source_ref_id = pull_request.source_ref_parts.commit_id
814 source_ref_id = pull_request.source_ref_parts.commit_id
811 target_ref_id = pull_request.target_ref_parts.commit_id
815 target_ref_id = pull_request.target_ref_parts.commit_id
812
816
813 new_diff = self._get_diff_from_pr_or_version(
817 new_diff = self._get_diff_from_pr_or_version(
814 source_repo, source_ref_id, target_ref_id, context=diff_context)
818 source_repo, source_ref_id, target_ref_id, context=diff_context)
815
819
816 old_diff_data = diffs.DiffProcessor(old_diff)
820 old_diff_data = diffs.DiffProcessor(old_diff)
817 old_diff_data.prepare()
821 old_diff_data.prepare()
818 new_diff_data = diffs.DiffProcessor(new_diff)
822 new_diff_data = diffs.DiffProcessor(new_diff)
819 new_diff_data.prepare()
823 new_diff_data.prepare()
820
824
821 return old_diff_data, new_diff_data
825 return old_diff_data, new_diff_data
822
826
823 def _link_comments_to_version(self, pull_request_version):
827 def _link_comments_to_version(self, pull_request_version):
824 """
828 """
825 Link all unlinked comments of this pull request to the given version.
829 Link all unlinked comments of this pull request to the given version.
826
830
827 :param pull_request_version: The `PullRequestVersion` to which
831 :param pull_request_version: The `PullRequestVersion` to which
828 the comments shall be linked.
832 the comments shall be linked.
829
833
830 """
834 """
831 pull_request = pull_request_version.pull_request
835 pull_request = pull_request_version.pull_request
832 comments = ChangesetComment.query()\
836 comments = ChangesetComment.query()\
833 .filter(
837 .filter(
834 # TODO: johbo: Should we query for the repo at all here?
838 # TODO: johbo: Should we query for the repo at all here?
835 # Pending decision on how comments of PRs are to be related
839 # Pending decision on how comments of PRs are to be related
836 # to either the source repo, the target repo or no repo at all.
840 # to either the source repo, the target repo or no repo at all.
837 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
841 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
838 ChangesetComment.pull_request == pull_request,
842 ChangesetComment.pull_request == pull_request,
839 ChangesetComment.pull_request_version == None)\
843 ChangesetComment.pull_request_version == None)\
840 .order_by(ChangesetComment.comment_id.asc())
844 .order_by(ChangesetComment.comment_id.asc())
841
845
842 # TODO: johbo: Find out why this breaks if it is done in a bulk
846 # TODO: johbo: Find out why this breaks if it is done in a bulk
843 # operation.
847 # operation.
844 for comment in comments:
848 for comment in comments:
845 comment.pull_request_version_id = (
849 comment.pull_request_version_id = (
846 pull_request_version.pull_request_version_id)
850 pull_request_version.pull_request_version_id)
847 Session().add(comment)
851 Session().add(comment)
848
852
849 def _calculate_commit_id_changes(self, old_ids, new_ids):
853 def _calculate_commit_id_changes(self, old_ids, new_ids):
850 added = [x for x in new_ids if x not in old_ids]
854 added = [x for x in new_ids if x not in old_ids]
851 common = [x for x in new_ids if x in old_ids]
855 common = [x for x in new_ids if x in old_ids]
852 removed = [x for x in old_ids if x not in new_ids]
856 removed = [x for x in old_ids if x not in new_ids]
853 total = new_ids
857 total = new_ids
854 return ChangeTuple(added, common, removed, total)
858 return ChangeTuple(added, common, removed, total)
855
859
856 def _calculate_file_changes(self, old_diff_data, new_diff_data):
860 def _calculate_file_changes(self, old_diff_data, new_diff_data):
857
861
858 old_files = OrderedDict()
862 old_files = OrderedDict()
859 for diff_data in old_diff_data.parsed_diff:
863 for diff_data in old_diff_data.parsed_diff:
860 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
864 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
861
865
862 added_files = []
866 added_files = []
863 modified_files = []
867 modified_files = []
864 removed_files = []
868 removed_files = []
865 for diff_data in new_diff_data.parsed_diff:
869 for diff_data in new_diff_data.parsed_diff:
866 new_filename = diff_data['filename']
870 new_filename = diff_data['filename']
867 new_hash = md5_safe(diff_data['raw_diff'])
871 new_hash = md5_safe(diff_data['raw_diff'])
868
872
869 old_hash = old_files.get(new_filename)
873 old_hash = old_files.get(new_filename)
870 if not old_hash:
874 if not old_hash:
871 # file is not present in old diff, means it's added
875 # file is not present in old diff, means it's added
872 added_files.append(new_filename)
876 added_files.append(new_filename)
873 else:
877 else:
874 if new_hash != old_hash:
878 if new_hash != old_hash:
875 modified_files.append(new_filename)
879 modified_files.append(new_filename)
876 # now remove a file from old, since we have seen it already
880 # now remove a file from old, since we have seen it already
877 del old_files[new_filename]
881 del old_files[new_filename]
878
882
879 # removed files is when there are present in old, but not in NEW,
883 # removed files is when there are present in old, but not in NEW,
880 # since we remove old files that are present in new diff, left-overs
884 # since we remove old files that are present in new diff, left-overs
881 # if any should be the removed files
885 # if any should be the removed files
882 removed_files.extend(old_files.keys())
886 removed_files.extend(old_files.keys())
883
887
884 return FileChangeTuple(added_files, modified_files, removed_files)
888 return FileChangeTuple(added_files, modified_files, removed_files)
885
889
886 def _render_update_message(self, changes, file_changes):
890 def _render_update_message(self, changes, file_changes):
887 """
891 """
888 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
892 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
889 so it's always looking the same disregarding on which default
893 so it's always looking the same disregarding on which default
890 renderer system is using.
894 renderer system is using.
891
895
892 :param changes: changes named tuple
896 :param changes: changes named tuple
893 :param file_changes: file changes named tuple
897 :param file_changes: file changes named tuple
894
898
895 """
899 """
896 new_status = ChangesetStatus.get_status_lbl(
900 new_status = ChangesetStatus.get_status_lbl(
897 ChangesetStatus.STATUS_UNDER_REVIEW)
901 ChangesetStatus.STATUS_UNDER_REVIEW)
898
902
899 changed_files = (
903 changed_files = (
900 file_changes.added + file_changes.modified + file_changes.removed)
904 file_changes.added + file_changes.modified + file_changes.removed)
901
905
902 params = {
906 params = {
903 'under_review_label': new_status,
907 'under_review_label': new_status,
904 'added_commits': changes.added,
908 'added_commits': changes.added,
905 'removed_commits': changes.removed,
909 'removed_commits': changes.removed,
906 'changed_files': changed_files,
910 'changed_files': changed_files,
907 'added_files': file_changes.added,
911 'added_files': file_changes.added,
908 'modified_files': file_changes.modified,
912 'modified_files': file_changes.modified,
909 'removed_files': file_changes.removed,
913 'removed_files': file_changes.removed,
910 }
914 }
911 renderer = RstTemplateRenderer()
915 renderer = RstTemplateRenderer()
912 return renderer.render('pull_request_update.mako', **params)
916 return renderer.render('pull_request_update.mako', **params)
913
917
914 def edit(self, pull_request, title, description, user):
918 def edit(self, pull_request, title, description, user):
915 pull_request = self.__get_pull_request(pull_request)
919 pull_request = self.__get_pull_request(pull_request)
916 old_data = pull_request.get_api_data(with_merge_state=False)
920 old_data = pull_request.get_api_data(with_merge_state=False)
917 if pull_request.is_closed():
921 if pull_request.is_closed():
918 raise ValueError('This pull request is closed')
922 raise ValueError('This pull request is closed')
919 if title:
923 if title:
920 pull_request.title = title
924 pull_request.title = title
921 pull_request.description = description
925 pull_request.description = description
922 pull_request.updated_on = datetime.datetime.now()
926 pull_request.updated_on = datetime.datetime.now()
923 Session().add(pull_request)
927 Session().add(pull_request)
924 self._log_audit_action(
928 self._log_audit_action(
925 'repo.pull_request.edit', {'old_data': old_data},
929 'repo.pull_request.edit', {'old_data': old_data},
926 user, pull_request)
930 user, pull_request)
927
931
928 def update_reviewers(self, pull_request, reviewer_data, user):
932 def update_reviewers(self, pull_request, reviewer_data, user):
929 """
933 """
930 Update the reviewers in the pull request
934 Update the reviewers in the pull request
931
935
932 :param pull_request: the pr to update
936 :param pull_request: the pr to update
933 :param reviewer_data: list of tuples
937 :param reviewer_data: list of tuples
934 [(user, ['reason1', 'reason2'], mandatory_flag)]
938 [(user, ['reason1', 'reason2'], mandatory_flag)]
935 """
939 """
936
940
937 reviewers = {}
941 reviewers = {}
938 for user_id, reasons, mandatory in reviewer_data:
942 for user_id, reasons, mandatory in reviewer_data:
939 if isinstance(user_id, (int, basestring)):
943 if isinstance(user_id, (int, basestring)):
940 user_id = self._get_user(user_id).user_id
944 user_id = self._get_user(user_id).user_id
941 reviewers[user_id] = {
945 reviewers[user_id] = {
942 'reasons': reasons, 'mandatory': mandatory}
946 'reasons': reasons, 'mandatory': mandatory}
943
947
944 reviewers_ids = set(reviewers.keys())
948 reviewers_ids = set(reviewers.keys())
945 pull_request = self.__get_pull_request(pull_request)
949 pull_request = self.__get_pull_request(pull_request)
946 current_reviewers = PullRequestReviewers.query()\
950 current_reviewers = PullRequestReviewers.query()\
947 .filter(PullRequestReviewers.pull_request ==
951 .filter(PullRequestReviewers.pull_request ==
948 pull_request).all()
952 pull_request).all()
949 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
953 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
950
954
951 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
955 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
952 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
956 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
953
957
954 log.debug("Adding %s reviewers", ids_to_add)
958 log.debug("Adding %s reviewers", ids_to_add)
955 log.debug("Removing %s reviewers", ids_to_remove)
959 log.debug("Removing %s reviewers", ids_to_remove)
956 changed = False
960 changed = False
957 for uid in ids_to_add:
961 for uid in ids_to_add:
958 changed = True
962 changed = True
959 _usr = self._get_user(uid)
963 _usr = self._get_user(uid)
960 reviewer = PullRequestReviewers()
964 reviewer = PullRequestReviewers()
961 reviewer.user = _usr
965 reviewer.user = _usr
962 reviewer.pull_request = pull_request
966 reviewer.pull_request = pull_request
963 reviewer.reasons = reviewers[uid]['reasons']
967 reviewer.reasons = reviewers[uid]['reasons']
964 # NOTE(marcink): mandatory shouldn't be changed now
968 # NOTE(marcink): mandatory shouldn't be changed now
965 # reviewer.mandatory = reviewers[uid]['reasons']
969 # reviewer.mandatory = reviewers[uid]['reasons']
966 Session().add(reviewer)
970 Session().add(reviewer)
967 self._log_audit_action(
971 self._log_audit_action(
968 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
972 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
969 user, pull_request)
973 user, pull_request)
970
974
971 for uid in ids_to_remove:
975 for uid in ids_to_remove:
972 changed = True
976 changed = True
973 reviewers = PullRequestReviewers.query()\
977 reviewers = PullRequestReviewers.query()\
974 .filter(PullRequestReviewers.user_id == uid,
978 .filter(PullRequestReviewers.user_id == uid,
975 PullRequestReviewers.pull_request == pull_request)\
979 PullRequestReviewers.pull_request == pull_request)\
976 .all()
980 .all()
977 # use .all() in case we accidentally added the same person twice
981 # use .all() in case we accidentally added the same person twice
978 # this CAN happen due to the lack of DB checks
982 # this CAN happen due to the lack of DB checks
979 for obj in reviewers:
983 for obj in reviewers:
980 old_data = obj.get_dict()
984 old_data = obj.get_dict()
981 Session().delete(obj)
985 Session().delete(obj)
982 self._log_audit_action(
986 self._log_audit_action(
983 'repo.pull_request.reviewer.delete',
987 'repo.pull_request.reviewer.delete',
984 {'old_data': old_data}, user, pull_request)
988 {'old_data': old_data}, user, pull_request)
985
989
986 if changed:
990 if changed:
987 pull_request.updated_on = datetime.datetime.now()
991 pull_request.updated_on = datetime.datetime.now()
988 Session().add(pull_request)
992 Session().add(pull_request)
989
993
990 self.notify_reviewers(pull_request, ids_to_add)
994 self.notify_reviewers(pull_request, ids_to_add)
991 return ids_to_add, ids_to_remove
995 return ids_to_add, ids_to_remove
992
996
993 def get_url(self, pull_request, request=None, permalink=False):
997 def get_url(self, pull_request, request=None, permalink=False):
994 if not request:
998 if not request:
995 request = get_current_request()
999 request = get_current_request()
996
1000
997 if permalink:
1001 if permalink:
998 return request.route_url(
1002 return request.route_url(
999 'pull_requests_global',
1003 'pull_requests_global',
1000 pull_request_id=pull_request.pull_request_id,)
1004 pull_request_id=pull_request.pull_request_id,)
1001 else:
1005 else:
1002 return request.route_url('pullrequest_show',
1006 return request.route_url('pullrequest_show',
1003 repo_name=safe_str(pull_request.target_repo.repo_name),
1007 repo_name=safe_str(pull_request.target_repo.repo_name),
1004 pull_request_id=pull_request.pull_request_id,)
1008 pull_request_id=pull_request.pull_request_id,)
1005
1009
1006 def get_shadow_clone_url(self, pull_request):
1010 def get_shadow_clone_url(self, pull_request):
1007 """
1011 """
1008 Returns qualified url pointing to the shadow repository. If this pull
1012 Returns qualified url pointing to the shadow repository. If this pull
1009 request is closed there is no shadow repository and ``None`` will be
1013 request is closed there is no shadow repository and ``None`` will be
1010 returned.
1014 returned.
1011 """
1015 """
1012 if pull_request.is_closed():
1016 if pull_request.is_closed():
1013 return None
1017 return None
1014 else:
1018 else:
1015 pr_url = urllib.unquote(self.get_url(pull_request))
1019 pr_url = urllib.unquote(self.get_url(pull_request))
1016 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1020 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1017
1021
1018 def notify_reviewers(self, pull_request, reviewers_ids):
1022 def notify_reviewers(self, pull_request, reviewers_ids):
1019 # notification to reviewers
1023 # notification to reviewers
1020 if not reviewers_ids:
1024 if not reviewers_ids:
1021 return
1025 return
1022
1026
1023 pull_request_obj = pull_request
1027 pull_request_obj = pull_request
1024 # get the current participants of this pull request
1028 # get the current participants of this pull request
1025 recipients = reviewers_ids
1029 recipients = reviewers_ids
1026 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1030 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1027
1031
1028 pr_source_repo = pull_request_obj.source_repo
1032 pr_source_repo = pull_request_obj.source_repo
1029 pr_target_repo = pull_request_obj.target_repo
1033 pr_target_repo = pull_request_obj.target_repo
1030
1034
1031 pr_url = h.route_url('pullrequest_show',
1035 pr_url = h.route_url('pullrequest_show',
1032 repo_name=pr_target_repo.repo_name,
1036 repo_name=pr_target_repo.repo_name,
1033 pull_request_id=pull_request_obj.pull_request_id,)
1037 pull_request_id=pull_request_obj.pull_request_id,)
1034
1038
1035 # set some variables for email notification
1039 # set some variables for email notification
1036 pr_target_repo_url = h.route_url(
1040 pr_target_repo_url = h.route_url(
1037 'repo_summary', repo_name=pr_target_repo.repo_name)
1041 'repo_summary', repo_name=pr_target_repo.repo_name)
1038
1042
1039 pr_source_repo_url = h.route_url(
1043 pr_source_repo_url = h.route_url(
1040 'repo_summary', repo_name=pr_source_repo.repo_name)
1044 'repo_summary', repo_name=pr_source_repo.repo_name)
1041
1045
1042 # pull request specifics
1046 # pull request specifics
1043 pull_request_commits = [
1047 pull_request_commits = [
1044 (x.raw_id, x.message)
1048 (x.raw_id, x.message)
1045 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1049 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1046
1050
1047 kwargs = {
1051 kwargs = {
1048 'user': pull_request.author,
1052 'user': pull_request.author,
1049 'pull_request': pull_request_obj,
1053 'pull_request': pull_request_obj,
1050 'pull_request_commits': pull_request_commits,
1054 'pull_request_commits': pull_request_commits,
1051
1055
1052 'pull_request_target_repo': pr_target_repo,
1056 'pull_request_target_repo': pr_target_repo,
1053 'pull_request_target_repo_url': pr_target_repo_url,
1057 'pull_request_target_repo_url': pr_target_repo_url,
1054
1058
1055 'pull_request_source_repo': pr_source_repo,
1059 'pull_request_source_repo': pr_source_repo,
1056 'pull_request_source_repo_url': pr_source_repo_url,
1060 'pull_request_source_repo_url': pr_source_repo_url,
1057
1061
1058 'pull_request_url': pr_url,
1062 'pull_request_url': pr_url,
1059 }
1063 }
1060
1064
1061 # pre-generate the subject for notification itself
1065 # pre-generate the subject for notification itself
1062 (subject,
1066 (subject,
1063 _h, _e, # we don't care about those
1067 _h, _e, # we don't care about those
1064 body_plaintext) = EmailNotificationModel().render_email(
1068 body_plaintext) = EmailNotificationModel().render_email(
1065 notification_type, **kwargs)
1069 notification_type, **kwargs)
1066
1070
1067 # create notification objects, and emails
1071 # create notification objects, and emails
1068 NotificationModel().create(
1072 NotificationModel().create(
1069 created_by=pull_request.author,
1073 created_by=pull_request.author,
1070 notification_subject=subject,
1074 notification_subject=subject,
1071 notification_body=body_plaintext,
1075 notification_body=body_plaintext,
1072 notification_type=notification_type,
1076 notification_type=notification_type,
1073 recipients=recipients,
1077 recipients=recipients,
1074 email_kwargs=kwargs,
1078 email_kwargs=kwargs,
1075 )
1079 )
1076
1080
1077 def delete(self, pull_request, user):
1081 def delete(self, pull_request, user):
1078 pull_request = self.__get_pull_request(pull_request)
1082 pull_request = self.__get_pull_request(pull_request)
1079 old_data = pull_request.get_api_data(with_merge_state=False)
1083 old_data = pull_request.get_api_data(with_merge_state=False)
1080 self._cleanup_merge_workspace(pull_request)
1084 self._cleanup_merge_workspace(pull_request)
1081 self._log_audit_action(
1085 self._log_audit_action(
1082 'repo.pull_request.delete', {'old_data': old_data},
1086 'repo.pull_request.delete', {'old_data': old_data},
1083 user, pull_request)
1087 user, pull_request)
1084 Session().delete(pull_request)
1088 Session().delete(pull_request)
1085
1089
1086 def close_pull_request(self, pull_request, user):
1090 def close_pull_request(self, pull_request, user):
1087 pull_request = self.__get_pull_request(pull_request)
1091 pull_request = self.__get_pull_request(pull_request)
1088 self._cleanup_merge_workspace(pull_request)
1092 self._cleanup_merge_workspace(pull_request)
1089 pull_request.status = PullRequest.STATUS_CLOSED
1093 pull_request.status = PullRequest.STATUS_CLOSED
1090 pull_request.updated_on = datetime.datetime.now()
1094 pull_request.updated_on = datetime.datetime.now()
1091 Session().add(pull_request)
1095 Session().add(pull_request)
1092 self._trigger_pull_request_hook(
1096 self._trigger_pull_request_hook(
1093 pull_request, pull_request.author, 'close')
1097 pull_request, pull_request.author, 'close')
1094
1098
1095 pr_data = pull_request.get_api_data(with_merge_state=False)
1099 pr_data = pull_request.get_api_data(with_merge_state=False)
1096 self._log_audit_action(
1100 self._log_audit_action(
1097 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1101 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1098
1102
1099 def close_pull_request_with_comment(
1103 def close_pull_request_with_comment(
1100 self, pull_request, user, repo, message=None):
1104 self, pull_request, user, repo, message=None):
1101
1105
1102 pull_request_review_status = pull_request.calculated_review_status()
1106 pull_request_review_status = pull_request.calculated_review_status()
1103
1107
1104 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1108 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1105 # approved only if we have voting consent
1109 # approved only if we have voting consent
1106 status = ChangesetStatus.STATUS_APPROVED
1110 status = ChangesetStatus.STATUS_APPROVED
1107 else:
1111 else:
1108 status = ChangesetStatus.STATUS_REJECTED
1112 status = ChangesetStatus.STATUS_REJECTED
1109 status_lbl = ChangesetStatus.get_status_lbl(status)
1113 status_lbl = ChangesetStatus.get_status_lbl(status)
1110
1114
1111 default_message = (
1115 default_message = (
1112 _('Closing with status change {transition_icon} {status}.')
1116 'Closing with status change {transition_icon} {status}.'
1113 ).format(transition_icon='>', status=status_lbl)
1117 ).format(transition_icon='>', status=status_lbl)
1114 text = message or default_message
1118 text = message or default_message
1115
1119
1116 # create a comment, and link it to new status
1120 # create a comment, and link it to new status
1117 comment = CommentsModel().create(
1121 comment = CommentsModel().create(
1118 text=text,
1122 text=text,
1119 repo=repo.repo_id,
1123 repo=repo.repo_id,
1120 user=user.user_id,
1124 user=user.user_id,
1121 pull_request=pull_request.pull_request_id,
1125 pull_request=pull_request.pull_request_id,
1122 status_change=status_lbl,
1126 status_change=status_lbl,
1123 status_change_type=status,
1127 status_change_type=status,
1124 closing_pr=True
1128 closing_pr=True
1125 )
1129 )
1126
1130
1127 # calculate old status before we change it
1131 # calculate old status before we change it
1128 old_calculated_status = pull_request.calculated_review_status()
1132 old_calculated_status = pull_request.calculated_review_status()
1129 ChangesetStatusModel().set_status(
1133 ChangesetStatusModel().set_status(
1130 repo.repo_id,
1134 repo.repo_id,
1131 status,
1135 status,
1132 user.user_id,
1136 user.user_id,
1133 comment=comment,
1137 comment=comment,
1134 pull_request=pull_request.pull_request_id
1138 pull_request=pull_request.pull_request_id
1135 )
1139 )
1136
1140
1137 Session().flush()
1141 Session().flush()
1138 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1142 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1139 # we now calculate the status of pull request again, and based on that
1143 # we now calculate the status of pull request again, and based on that
1140 # calculation trigger status change. This might happen in cases
1144 # calculation trigger status change. This might happen in cases
1141 # that non-reviewer admin closes a pr, which means his vote doesn't
1145 # that non-reviewer admin closes a pr, which means his vote doesn't
1142 # change the status, while if he's a reviewer this might change it.
1146 # change the status, while if he's a reviewer this might change it.
1143 calculated_status = pull_request.calculated_review_status()
1147 calculated_status = pull_request.calculated_review_status()
1144 if old_calculated_status != calculated_status:
1148 if old_calculated_status != calculated_status:
1145 self._trigger_pull_request_hook(
1149 self._trigger_pull_request_hook(
1146 pull_request, user, 'review_status_change')
1150 pull_request, user, 'review_status_change')
1147
1151
1148 # finally close the PR
1152 # finally close the PR
1149 PullRequestModel().close_pull_request(
1153 PullRequestModel().close_pull_request(
1150 pull_request.pull_request_id, user)
1154 pull_request.pull_request_id, user)
1151
1155
1152 return comment, status
1156 return comment, status
1153
1157
1154 def merge_status(self, pull_request):
1158 def merge_status(self, pull_request, translator=None):
1159 _ = translator or get_current_request().translate
1160
1155 if not self._is_merge_enabled(pull_request):
1161 if not self._is_merge_enabled(pull_request):
1156 return False, _('Server-side pull request merging is disabled.')
1162 return False, _('Server-side pull request merging is disabled.')
1157 if pull_request.is_closed():
1163 if pull_request.is_closed():
1158 return False, _('This pull request is closed.')
1164 return False, _('This pull request is closed.')
1159 merge_possible, msg = self._check_repo_requirements(
1165 merge_possible, msg = self._check_repo_requirements(
1160 target=pull_request.target_repo, source=pull_request.source_repo)
1166 target=pull_request.target_repo, source=pull_request.source_repo,
1167 translator=_)
1161 if not merge_possible:
1168 if not merge_possible:
1162 return merge_possible, msg
1169 return merge_possible, msg
1163
1170
1164 try:
1171 try:
1165 resp = self._try_merge(pull_request)
1172 resp = self._try_merge(pull_request)
1166 log.debug("Merge response: %s", resp)
1173 log.debug("Merge response: %s", resp)
1167 status = resp.possible, self.merge_status_message(
1174 status = resp.possible, self.merge_status_message(
1168 resp.failure_reason)
1175 resp.failure_reason)
1169 except NotImplementedError:
1176 except NotImplementedError:
1170 status = False, _('Pull request merging is not supported.')
1177 status = False, _('Pull request merging is not supported.')
1171
1178
1172 return status
1179 return status
1173
1180
1174 def _check_repo_requirements(self, target, source):
1181 def _check_repo_requirements(self, target, source, translator):
1175 """
1182 """
1176 Check if `target` and `source` have compatible requirements.
1183 Check if `target` and `source` have compatible requirements.
1177
1184
1178 Currently this is just checking for largefiles.
1185 Currently this is just checking for largefiles.
1179 """
1186 """
1187 _ = translator
1180 target_has_largefiles = self._has_largefiles(target)
1188 target_has_largefiles = self._has_largefiles(target)
1181 source_has_largefiles = self._has_largefiles(source)
1189 source_has_largefiles = self._has_largefiles(source)
1182 merge_possible = True
1190 merge_possible = True
1183 message = u''
1191 message = u''
1184
1192
1185 if target_has_largefiles != source_has_largefiles:
1193 if target_has_largefiles != source_has_largefiles:
1186 merge_possible = False
1194 merge_possible = False
1187 if source_has_largefiles:
1195 if source_has_largefiles:
1188 message = _(
1196 message = _(
1189 'Target repository large files support is disabled.')
1197 'Target repository large files support is disabled.')
1190 else:
1198 else:
1191 message = _(
1199 message = _(
1192 'Source repository large files support is disabled.')
1200 'Source repository large files support is disabled.')
1193
1201
1194 return merge_possible, message
1202 return merge_possible, message
1195
1203
1196 def _has_largefiles(self, repo):
1204 def _has_largefiles(self, repo):
1197 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1205 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1198 'extensions', 'largefiles')
1206 'extensions', 'largefiles')
1199 return largefiles_ui and largefiles_ui[0].active
1207 return largefiles_ui and largefiles_ui[0].active
1200
1208
1201 def _try_merge(self, pull_request):
1209 def _try_merge(self, pull_request):
1202 """
1210 """
1203 Try to merge the pull request and return the merge status.
1211 Try to merge the pull request and return the merge status.
1204 """
1212 """
1205 log.debug(
1213 log.debug(
1206 "Trying out if the pull request %s can be merged.",
1214 "Trying out if the pull request %s can be merged.",
1207 pull_request.pull_request_id)
1215 pull_request.pull_request_id)
1208 target_vcs = pull_request.target_repo.scm_instance()
1216 target_vcs = pull_request.target_repo.scm_instance()
1209
1217
1210 # Refresh the target reference.
1218 # Refresh the target reference.
1211 try:
1219 try:
1212 target_ref = self._refresh_reference(
1220 target_ref = self._refresh_reference(
1213 pull_request.target_ref_parts, target_vcs)
1221 pull_request.target_ref_parts, target_vcs)
1214 except CommitDoesNotExistError:
1222 except CommitDoesNotExistError:
1215 merge_state = MergeResponse(
1223 merge_state = MergeResponse(
1216 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1224 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1217 return merge_state
1225 return merge_state
1218
1226
1219 target_locked = pull_request.target_repo.locked
1227 target_locked = pull_request.target_repo.locked
1220 if target_locked and target_locked[0]:
1228 if target_locked and target_locked[0]:
1221 log.debug("The target repository is locked.")
1229 log.debug("The target repository is locked.")
1222 merge_state = MergeResponse(
1230 merge_state = MergeResponse(
1223 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1231 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1224 elif self._needs_merge_state_refresh(pull_request, target_ref):
1232 elif self._needs_merge_state_refresh(pull_request, target_ref):
1225 log.debug("Refreshing the merge status of the repository.")
1233 log.debug("Refreshing the merge status of the repository.")
1226 merge_state = self._refresh_merge_state(
1234 merge_state = self._refresh_merge_state(
1227 pull_request, target_vcs, target_ref)
1235 pull_request, target_vcs, target_ref)
1228 else:
1236 else:
1229 possible = pull_request.\
1237 possible = pull_request.\
1230 last_merge_status == MergeFailureReason.NONE
1238 last_merge_status == MergeFailureReason.NONE
1231 merge_state = MergeResponse(
1239 merge_state = MergeResponse(
1232 possible, False, None, pull_request.last_merge_status)
1240 possible, False, None, pull_request.last_merge_status)
1233
1241
1234 return merge_state
1242 return merge_state
1235
1243
1236 def _refresh_reference(self, reference, vcs_repository):
1244 def _refresh_reference(self, reference, vcs_repository):
1237 if reference.type in ('branch', 'book'):
1245 if reference.type in ('branch', 'book'):
1238 name_or_id = reference.name
1246 name_or_id = reference.name
1239 else:
1247 else:
1240 name_or_id = reference.commit_id
1248 name_or_id = reference.commit_id
1241 refreshed_commit = vcs_repository.get_commit(name_or_id)
1249 refreshed_commit = vcs_repository.get_commit(name_or_id)
1242 refreshed_reference = Reference(
1250 refreshed_reference = Reference(
1243 reference.type, reference.name, refreshed_commit.raw_id)
1251 reference.type, reference.name, refreshed_commit.raw_id)
1244 return refreshed_reference
1252 return refreshed_reference
1245
1253
1246 def _needs_merge_state_refresh(self, pull_request, target_reference):
1254 def _needs_merge_state_refresh(self, pull_request, target_reference):
1247 return not(
1255 return not(
1248 pull_request.revisions and
1256 pull_request.revisions and
1249 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1257 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1250 target_reference.commit_id == pull_request._last_merge_target_rev)
1258 target_reference.commit_id == pull_request._last_merge_target_rev)
1251
1259
1252 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1260 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1253 workspace_id = self._workspace_id(pull_request)
1261 workspace_id = self._workspace_id(pull_request)
1254 source_vcs = pull_request.source_repo.scm_instance()
1262 source_vcs = pull_request.source_repo.scm_instance()
1255 use_rebase = self._use_rebase_for_merging(pull_request)
1263 use_rebase = self._use_rebase_for_merging(pull_request)
1256 close_branch = self._close_branch_before_merging(pull_request)
1264 close_branch = self._close_branch_before_merging(pull_request)
1257 merge_state = target_vcs.merge(
1265 merge_state = target_vcs.merge(
1258 target_reference, source_vcs, pull_request.source_ref_parts,
1266 target_reference, source_vcs, pull_request.source_ref_parts,
1259 workspace_id, dry_run=True, use_rebase=use_rebase,
1267 workspace_id, dry_run=True, use_rebase=use_rebase,
1260 close_branch=close_branch)
1268 close_branch=close_branch)
1261
1269
1262 # Do not store the response if there was an unknown error.
1270 # Do not store the response if there was an unknown error.
1263 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1271 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1264 pull_request._last_merge_source_rev = \
1272 pull_request._last_merge_source_rev = \
1265 pull_request.source_ref_parts.commit_id
1273 pull_request.source_ref_parts.commit_id
1266 pull_request._last_merge_target_rev = target_reference.commit_id
1274 pull_request._last_merge_target_rev = target_reference.commit_id
1267 pull_request.last_merge_status = merge_state.failure_reason
1275 pull_request.last_merge_status = merge_state.failure_reason
1268 pull_request.shadow_merge_ref = merge_state.merge_ref
1276 pull_request.shadow_merge_ref = merge_state.merge_ref
1269 Session().add(pull_request)
1277 Session().add(pull_request)
1270 Session().commit()
1278 Session().commit()
1271
1279
1272 return merge_state
1280 return merge_state
1273
1281
1274 def _workspace_id(self, pull_request):
1282 def _workspace_id(self, pull_request):
1275 workspace_id = 'pr-%s' % pull_request.pull_request_id
1283 workspace_id = 'pr-%s' % pull_request.pull_request_id
1276 return workspace_id
1284 return workspace_id
1277
1285
1278 def merge_status_message(self, status_code):
1286 def merge_status_message(self, status_code):
1279 """
1287 """
1280 Return a human friendly error message for the given merge status code.
1288 Return a human friendly error message for the given merge status code.
1281 """
1289 """
1282 return self.MERGE_STATUS_MESSAGES[status_code]
1290 return self.MERGE_STATUS_MESSAGES[status_code]
1283
1291
1284 def generate_repo_data(self, repo, commit_id=None, branch=None,
1292 def generate_repo_data(self, repo, commit_id=None, branch=None,
1285 bookmark=None):
1293 bookmark=None, translator=None):
1294
1286 all_refs, selected_ref = \
1295 all_refs, selected_ref = \
1287 self._get_repo_pullrequest_sources(
1296 self._get_repo_pullrequest_sources(
1288 repo.scm_instance(), commit_id=commit_id,
1297 repo.scm_instance(), commit_id=commit_id,
1289 branch=branch, bookmark=bookmark)
1298 branch=branch, bookmark=bookmark, translator=translator)
1290
1299
1291 refs_select2 = []
1300 refs_select2 = []
1292 for element in all_refs:
1301 for element in all_refs:
1293 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1302 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1294 refs_select2.append({'text': element[1], 'children': children})
1303 refs_select2.append({'text': element[1], 'children': children})
1295
1304
1296 return {
1305 return {
1297 'user': {
1306 'user': {
1298 'user_id': repo.user.user_id,
1307 'user_id': repo.user.user_id,
1299 'username': repo.user.username,
1308 'username': repo.user.username,
1300 'firstname': repo.user.first_name,
1309 'firstname': repo.user.first_name,
1301 'lastname': repo.user.last_name,
1310 'lastname': repo.user.last_name,
1302 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1311 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1303 },
1312 },
1304 'description': h.chop_at_smart(repo.description_safe, '\n'),
1313 'description': h.chop_at_smart(repo.description_safe, '\n'),
1305 'refs': {
1314 'refs': {
1306 'all_refs': all_refs,
1315 'all_refs': all_refs,
1307 'selected_ref': selected_ref,
1316 'selected_ref': selected_ref,
1308 'select2_refs': refs_select2
1317 'select2_refs': refs_select2
1309 }
1318 }
1310 }
1319 }
1311
1320
1312 def generate_pullrequest_title(self, source, source_ref, target):
1321 def generate_pullrequest_title(self, source, source_ref, target):
1313 return u'{source}#{at_ref} to {target}'.format(
1322 return u'{source}#{at_ref} to {target}'.format(
1314 source=source,
1323 source=source,
1315 at_ref=source_ref,
1324 at_ref=source_ref,
1316 target=target,
1325 target=target,
1317 )
1326 )
1318
1327
1319 def _cleanup_merge_workspace(self, pull_request):
1328 def _cleanup_merge_workspace(self, pull_request):
1320 # Merging related cleanup
1329 # Merging related cleanup
1321 target_scm = pull_request.target_repo.scm_instance()
1330 target_scm = pull_request.target_repo.scm_instance()
1322 workspace_id = 'pr-%s' % pull_request.pull_request_id
1331 workspace_id = 'pr-%s' % pull_request.pull_request_id
1323
1332
1324 try:
1333 try:
1325 target_scm.cleanup_merge_workspace(workspace_id)
1334 target_scm.cleanup_merge_workspace(workspace_id)
1326 except NotImplementedError:
1335 except NotImplementedError:
1327 pass
1336 pass
1328
1337
1329 def _get_repo_pullrequest_sources(
1338 def _get_repo_pullrequest_sources(
1330 self, repo, commit_id=None, branch=None, bookmark=None):
1339 self, repo, commit_id=None, branch=None, bookmark=None,
1340 translator=None):
1331 """
1341 """
1332 Return a structure with repo's interesting commits, suitable for
1342 Return a structure with repo's interesting commits, suitable for
1333 the selectors in pullrequest controller
1343 the selectors in pullrequest controller
1334
1344
1335 :param commit_id: a commit that must be in the list somehow
1345 :param commit_id: a commit that must be in the list somehow
1336 and selected by default
1346 and selected by default
1337 :param branch: a branch that must be in the list and selected
1347 :param branch: a branch that must be in the list and selected
1338 by default - even if closed
1348 by default - even if closed
1339 :param bookmark: a bookmark that must be in the list and selected
1349 :param bookmark: a bookmark that must be in the list and selected
1340 """
1350 """
1351 _ = translator or get_current_request().translate
1341
1352
1342 commit_id = safe_str(commit_id) if commit_id else None
1353 commit_id = safe_str(commit_id) if commit_id else None
1343 branch = safe_str(branch) if branch else None
1354 branch = safe_str(branch) if branch else None
1344 bookmark = safe_str(bookmark) if bookmark else None
1355 bookmark = safe_str(bookmark) if bookmark else None
1345
1356
1346 selected = None
1357 selected = None
1347
1358
1348 # order matters: first source that has commit_id in it will be selected
1359 # order matters: first source that has commit_id in it will be selected
1349 sources = []
1360 sources = []
1350 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1361 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1351 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1362 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1352
1363
1353 if commit_id:
1364 if commit_id:
1354 ref_commit = (h.short_id(commit_id), commit_id)
1365 ref_commit = (h.short_id(commit_id), commit_id)
1355 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1366 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1356
1367
1357 sources.append(
1368 sources.append(
1358 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1369 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1359 )
1370 )
1360
1371
1361 groups = []
1372 groups = []
1362 for group_key, ref_list, group_name, match in sources:
1373 for group_key, ref_list, group_name, match in sources:
1363 group_refs = []
1374 group_refs = []
1364 for ref_name, ref_id in ref_list:
1375 for ref_name, ref_id in ref_list:
1365 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1376 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1366 group_refs.append((ref_key, ref_name))
1377 group_refs.append((ref_key, ref_name))
1367
1378
1368 if not selected:
1379 if not selected:
1369 if set([commit_id, match]) & set([ref_id, ref_name]):
1380 if set([commit_id, match]) & set([ref_id, ref_name]):
1370 selected = ref_key
1381 selected = ref_key
1371
1382
1372 if group_refs:
1383 if group_refs:
1373 groups.append((group_refs, group_name))
1384 groups.append((group_refs, group_name))
1374
1385
1375 if not selected:
1386 if not selected:
1376 ref = commit_id or branch or bookmark
1387 ref = commit_id or branch or bookmark
1377 if ref:
1388 if ref:
1378 raise CommitDoesNotExistError(
1389 raise CommitDoesNotExistError(
1379 'No commit refs could be found matching: %s' % ref)
1390 'No commit refs could be found matching: %s' % ref)
1380 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1391 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1381 selected = 'branch:%s:%s' % (
1392 selected = 'branch:%s:%s' % (
1382 repo.DEFAULT_BRANCH_NAME,
1393 repo.DEFAULT_BRANCH_NAME,
1383 repo.branches[repo.DEFAULT_BRANCH_NAME]
1394 repo.branches[repo.DEFAULT_BRANCH_NAME]
1384 )
1395 )
1385 elif repo.commit_ids:
1396 elif repo.commit_ids:
1386 rev = repo.commit_ids[0]
1397 rev = repo.commit_ids[0]
1387 selected = 'rev:%s:%s' % (rev, rev)
1398 selected = 'rev:%s:%s' % (rev, rev)
1388 else:
1399 else:
1389 raise EmptyRepositoryError()
1400 raise EmptyRepositoryError()
1390 return groups, selected
1401 return groups, selected
1391
1402
1392 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1403 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1393 return self._get_diff_from_pr_or_version(
1404 return self._get_diff_from_pr_or_version(
1394 source_repo, source_ref_id, target_ref_id, context=context)
1405 source_repo, source_ref_id, target_ref_id, context=context)
1395
1406
1396 def _get_diff_from_pr_or_version(
1407 def _get_diff_from_pr_or_version(
1397 self, source_repo, source_ref_id, target_ref_id, context):
1408 self, source_repo, source_ref_id, target_ref_id, context):
1398 target_commit = source_repo.get_commit(
1409 target_commit = source_repo.get_commit(
1399 commit_id=safe_str(target_ref_id))
1410 commit_id=safe_str(target_ref_id))
1400 source_commit = source_repo.get_commit(
1411 source_commit = source_repo.get_commit(
1401 commit_id=safe_str(source_ref_id))
1412 commit_id=safe_str(source_ref_id))
1402 if isinstance(source_repo, Repository):
1413 if isinstance(source_repo, Repository):
1403 vcs_repo = source_repo.scm_instance()
1414 vcs_repo = source_repo.scm_instance()
1404 else:
1415 else:
1405 vcs_repo = source_repo
1416 vcs_repo = source_repo
1406
1417
1407 # TODO: johbo: In the context of an update, we cannot reach
1418 # TODO: johbo: In the context of an update, we cannot reach
1408 # the old commit anymore with our normal mechanisms. It needs
1419 # the old commit anymore with our normal mechanisms. It needs
1409 # some sort of special support in the vcs layer to avoid this
1420 # some sort of special support in the vcs layer to avoid this
1410 # workaround.
1421 # workaround.
1411 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1422 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1412 vcs_repo.alias == 'git'):
1423 vcs_repo.alias == 'git'):
1413 source_commit.raw_id = safe_str(source_ref_id)
1424 source_commit.raw_id = safe_str(source_ref_id)
1414
1425
1415 log.debug('calculating diff between '
1426 log.debug('calculating diff between '
1416 'source_ref:%s and target_ref:%s for repo `%s`',
1427 'source_ref:%s and target_ref:%s for repo `%s`',
1417 target_ref_id, source_ref_id,
1428 target_ref_id, source_ref_id,
1418 safe_unicode(vcs_repo.path))
1429 safe_unicode(vcs_repo.path))
1419
1430
1420 vcs_diff = vcs_repo.get_diff(
1431 vcs_diff = vcs_repo.get_diff(
1421 commit1=target_commit, commit2=source_commit, context=context)
1432 commit1=target_commit, commit2=source_commit, context=context)
1422 return vcs_diff
1433 return vcs_diff
1423
1434
1424 def _is_merge_enabled(self, pull_request):
1435 def _is_merge_enabled(self, pull_request):
1425 return self._get_general_setting(
1436 return self._get_general_setting(
1426 pull_request, 'rhodecode_pr_merge_enabled')
1437 pull_request, 'rhodecode_pr_merge_enabled')
1427
1438
1428 def _use_rebase_for_merging(self, pull_request):
1439 def _use_rebase_for_merging(self, pull_request):
1429 repo_type = pull_request.target_repo.repo_type
1440 repo_type = pull_request.target_repo.repo_type
1430 if repo_type == 'hg':
1441 if repo_type == 'hg':
1431 return self._get_general_setting(
1442 return self._get_general_setting(
1432 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1443 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1433 elif repo_type == 'git':
1444 elif repo_type == 'git':
1434 return self._get_general_setting(
1445 return self._get_general_setting(
1435 pull_request, 'rhodecode_git_use_rebase_for_merging')
1446 pull_request, 'rhodecode_git_use_rebase_for_merging')
1436
1447
1437 return False
1448 return False
1438
1449
1439 def _close_branch_before_merging(self, pull_request):
1450 def _close_branch_before_merging(self, pull_request):
1440 repo_type = pull_request.target_repo.repo_type
1451 repo_type = pull_request.target_repo.repo_type
1441 if repo_type == 'hg':
1452 if repo_type == 'hg':
1442 return self._get_general_setting(
1453 return self._get_general_setting(
1443 pull_request, 'rhodecode_hg_close_branch_before_merging')
1454 pull_request, 'rhodecode_hg_close_branch_before_merging')
1444 elif repo_type == 'git':
1455 elif repo_type == 'git':
1445 return self._get_general_setting(
1456 return self._get_general_setting(
1446 pull_request, 'rhodecode_git_close_branch_before_merging')
1457 pull_request, 'rhodecode_git_close_branch_before_merging')
1447
1458
1448 return False
1459 return False
1449
1460
1450 def _get_general_setting(self, pull_request, settings_key, default=False):
1461 def _get_general_setting(self, pull_request, settings_key, default=False):
1451 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1462 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1452 settings = settings_model.get_general_settings()
1463 settings = settings_model.get_general_settings()
1453 return settings.get(settings_key, default)
1464 return settings.get(settings_key, default)
1454
1465
1455 def _log_audit_action(self, action, action_data, user, pull_request):
1466 def _log_audit_action(self, action, action_data, user, pull_request):
1456 audit_logger.store(
1467 audit_logger.store(
1457 action=action,
1468 action=action,
1458 action_data=action_data,
1469 action_data=action_data,
1459 user=user,
1470 user=user,
1460 repo=pull_request.target_repo)
1471 repo=pull_request.target_repo)
1461
1472
1462 def get_reviewer_functions(self):
1473 def get_reviewer_functions(self):
1463 """
1474 """
1464 Fetches functions for validation and fetching default reviewers.
1475 Fetches functions for validation and fetching default reviewers.
1465 If available we use the EE package, else we fallback to CE
1476 If available we use the EE package, else we fallback to CE
1466 package functions
1477 package functions
1467 """
1478 """
1468 try:
1479 try:
1469 from rc_reviewers.utils import get_default_reviewers_data
1480 from rc_reviewers.utils import get_default_reviewers_data
1470 from rc_reviewers.utils import validate_default_reviewers
1481 from rc_reviewers.utils import validate_default_reviewers
1471 except ImportError:
1482 except ImportError:
1472 from rhodecode.apps.repository.utils import \
1483 from rhodecode.apps.repository.utils import \
1473 get_default_reviewers_data
1484 get_default_reviewers_data
1474 from rhodecode.apps.repository.utils import \
1485 from rhodecode.apps.repository.utils import \
1475 validate_default_reviewers
1486 validate_default_reviewers
1476
1487
1477 return get_default_reviewers_data, validate_default_reviewers
1488 return get_default_reviewers_data, validate_default_reviewers
1478
1489
1479
1490
1480 class MergeCheck(object):
1491 class MergeCheck(object):
1481 """
1492 """
1482 Perform Merge Checks and returns a check object which stores information
1493 Perform Merge Checks and returns a check object which stores information
1483 about merge errors, and merge conditions
1494 about merge errors, and merge conditions
1484 """
1495 """
1485 TODO_CHECK = 'todo'
1496 TODO_CHECK = 'todo'
1486 PERM_CHECK = 'perm'
1497 PERM_CHECK = 'perm'
1487 REVIEW_CHECK = 'review'
1498 REVIEW_CHECK = 'review'
1488 MERGE_CHECK = 'merge'
1499 MERGE_CHECK = 'merge'
1489
1500
1490 def __init__(self):
1501 def __init__(self):
1491 self.review_status = None
1502 self.review_status = None
1492 self.merge_possible = None
1503 self.merge_possible = None
1493 self.merge_msg = ''
1504 self.merge_msg = ''
1494 self.failed = None
1505 self.failed = None
1495 self.errors = []
1506 self.errors = []
1496 self.error_details = OrderedDict()
1507 self.error_details = OrderedDict()
1497
1508
1498 def push_error(self, error_type, message, error_key, details):
1509 def push_error(self, error_type, message, error_key, details):
1499 self.failed = True
1510 self.failed = True
1500 self.errors.append([error_type, message])
1511 self.errors.append([error_type, message])
1501 self.error_details[error_key] = dict(
1512 self.error_details[error_key] = dict(
1502 details=details,
1513 details=details,
1503 error_type=error_type,
1514 error_type=error_type,
1504 message=message
1515 message=message
1505 )
1516 )
1506
1517
1507 @classmethod
1518 @classmethod
1508 def validate(cls, pull_request, user, fail_early=False, translator=None):
1519 def validate(cls, pull_request, user, translator, fail_early=False):
1509 # if migrated to pyramid...
1520 _ = translator
1510 # _ = lambda: translator or _ # use passed in translator if any
1511
1512 merge_check = cls()
1521 merge_check = cls()
1513
1522
1514 # permissions to merge
1523 # permissions to merge
1515 user_allowed_to_merge = PullRequestModel().check_user_merge(
1524 user_allowed_to_merge = PullRequestModel().check_user_merge(
1516 pull_request, user)
1525 pull_request, user)
1517 if not user_allowed_to_merge:
1526 if not user_allowed_to_merge:
1518 log.debug("MergeCheck: cannot merge, approval is pending.")
1527 log.debug("MergeCheck: cannot merge, approval is pending.")
1519
1528
1520 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1529 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1521 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1530 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1522 if fail_early:
1531 if fail_early:
1523 return merge_check
1532 return merge_check
1524
1533
1525 # review status, must be always present
1534 # review status, must be always present
1526 review_status = pull_request.calculated_review_status()
1535 review_status = pull_request.calculated_review_status()
1527 merge_check.review_status = review_status
1536 merge_check.review_status = review_status
1528
1537
1529 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1538 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1530 if not status_approved:
1539 if not status_approved:
1531 log.debug("MergeCheck: cannot merge, approval is pending.")
1540 log.debug("MergeCheck: cannot merge, approval is pending.")
1532
1541
1533 msg = _('Pull request reviewer approval is pending.')
1542 msg = _('Pull request reviewer approval is pending.')
1534
1543
1535 merge_check.push_error(
1544 merge_check.push_error(
1536 'warning', msg, cls.REVIEW_CHECK, review_status)
1545 'warning', msg, cls.REVIEW_CHECK, review_status)
1537
1546
1538 if fail_early:
1547 if fail_early:
1539 return merge_check
1548 return merge_check
1540
1549
1541 # left over TODOs
1550 # left over TODOs
1542 todos = CommentsModel().get_unresolved_todos(pull_request)
1551 todos = CommentsModel().get_unresolved_todos(pull_request)
1543 if todos:
1552 if todos:
1544 log.debug("MergeCheck: cannot merge, {} "
1553 log.debug("MergeCheck: cannot merge, {} "
1545 "unresolved todos left.".format(len(todos)))
1554 "unresolved todos left.".format(len(todos)))
1546
1555
1547 if len(todos) == 1:
1556 if len(todos) == 1:
1548 msg = _('Cannot merge, {} TODO still not resolved.').format(
1557 msg = _('Cannot merge, {} TODO still not resolved.').format(
1549 len(todos))
1558 len(todos))
1550 else:
1559 else:
1551 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1560 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1552 len(todos))
1561 len(todos))
1553
1562
1554 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1563 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1555
1564
1556 if fail_early:
1565 if fail_early:
1557 return merge_check
1566 return merge_check
1558
1567
1559 # merge possible
1568 # merge possible
1560 merge_status, msg = PullRequestModel().merge_status(pull_request)
1569 merge_status, msg = PullRequestModel().merge_status(
1570 pull_request, translator=translator)
1561 merge_check.merge_possible = merge_status
1571 merge_check.merge_possible = merge_status
1562 merge_check.merge_msg = msg
1572 merge_check.merge_msg = msg
1563 if not merge_status:
1573 if not merge_status:
1564 log.debug(
1574 log.debug(
1565 "MergeCheck: cannot merge, pull request merge not possible.")
1575 "MergeCheck: cannot merge, pull request merge not possible.")
1566 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1576 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1567
1577
1568 if fail_early:
1578 if fail_early:
1569 return merge_check
1579 return merge_check
1570
1580
1571 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1581 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1572 return merge_check
1582 return merge_check
1573
1583
1574 @classmethod
1584 @classmethod
1575 def get_merge_conditions(cls, pull_request):
1585 def get_merge_conditions(cls, pull_request, translator):
1586 _ = translator
1576 merge_details = {}
1587 merge_details = {}
1577
1588
1578 model = PullRequestModel()
1589 model = PullRequestModel()
1579 use_rebase = model._use_rebase_for_merging(pull_request)
1590 use_rebase = model._use_rebase_for_merging(pull_request)
1580
1591
1581 if use_rebase:
1592 if use_rebase:
1582 merge_details['merge_strategy'] = dict(
1593 merge_details['merge_strategy'] = dict(
1583 details={},
1594 details={},
1584 message=_('Merge strategy: rebase')
1595 message=_('Merge strategy: rebase')
1585 )
1596 )
1586 else:
1597 else:
1587 merge_details['merge_strategy'] = dict(
1598 merge_details['merge_strategy'] = dict(
1588 details={},
1599 details={},
1589 message=_('Merge strategy: explicit merge commit')
1600 message=_('Merge strategy: explicit merge commit')
1590 )
1601 )
1591
1602
1592 close_branch = model._close_branch_before_merging(pull_request)
1603 close_branch = model._close_branch_before_merging(pull_request)
1593 if close_branch:
1604 if close_branch:
1594 repo_type = pull_request.target_repo.repo_type
1605 repo_type = pull_request.target_repo.repo_type
1595 if repo_type == 'hg':
1606 if repo_type == 'hg':
1596 close_msg = _('Source branch will be closed after merge.')
1607 close_msg = _('Source branch will be closed after merge.')
1597 elif repo_type == 'git':
1608 elif repo_type == 'git':
1598 close_msg = _('Source branch will be deleted after merge.')
1609 close_msg = _('Source branch will be deleted after merge.')
1599
1610
1600 merge_details['close_branch'] = dict(
1611 merge_details['close_branch'] = dict(
1601 details={},
1612 details={},
1602 message=close_msg
1613 message=close_msg
1603 )
1614 )
1604
1615
1605 return merge_details
1616 return merge_details
1606
1617
1607 ChangeTuple = namedtuple('ChangeTuple',
1618 ChangeTuple = collections.namedtuple(
1608 ['added', 'common', 'removed', 'total'])
1619 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1609
1620
1610 FileChangeTuple = namedtuple('FileChangeTuple',
1621 FileChangeTuple = collections.namedtuple(
1611 ['added', 'modified', 'removed'])
1622 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,859 +1,859 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import CommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 @pytest.mark.usefixtures('config_stub')
44 @pytest.mark.usefixtures('config_stub')
45 class TestPullRequestModel(object):
45 class TestPullRequestModel(object):
46
46
47 @pytest.fixture
47 @pytest.fixture
48 def pull_request(self, request, backend, pr_util):
48 def pull_request(self, request, backend, pr_util):
49 """
49 """
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 self.merge_patcher = mock.patch.object(
53 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
55 False, False, None, MergeFailureReason.UNKNOWN))
56 self.workspace_remove_patcher = mock.patch.object(
56 self.workspace_remove_patcher = mock.patch.object(
57 BackendClass, 'cleanup_merge_workspace')
57 BackendClass, 'cleanup_merge_workspace')
58
58
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
61 self.comment_patcher = mock.patch(
61 self.comment_patcher = mock.patch(
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
63 self.comment_patcher.start()
63 self.comment_patcher.start()
64 self.notification_patcher = mock.patch(
64 self.notification_patcher = mock.patch(
65 'rhodecode.model.notification.NotificationModel.create')
65 'rhodecode.model.notification.NotificationModel.create')
66 self.notification_patcher.start()
66 self.notification_patcher.start()
67 self.helper_patcher = mock.patch(
67 self.helper_patcher = mock.patch(
68 'rhodecode.lib.helpers.url')
68 'rhodecode.lib.helpers.url')
69 self.helper_patcher.start()
69 self.helper_patcher.start()
70
70
71 self.hook_patcher = mock.patch.object(PullRequestModel,
71 self.hook_patcher = mock.patch.object(PullRequestModel,
72 '_trigger_pull_request_hook')
72 '_trigger_pull_request_hook')
73 self.hook_mock = self.hook_patcher.start()
73 self.hook_mock = self.hook_patcher.start()
74
74
75 self.invalidation_patcher = mock.patch(
75 self.invalidation_patcher = mock.patch(
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
77 self.invalidation_mock = self.invalidation_patcher.start()
77 self.invalidation_mock = self.invalidation_patcher.start()
78
78
79 self.pull_request = pr_util.create_pull_request(
79 self.pull_request = pr_util.create_pull_request(
80 mergeable=True, name_suffix=u'Δ…Δ‡')
80 mergeable=True, name_suffix=u'Δ…Δ‡')
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
84
84
85 @request.addfinalizer
85 @request.addfinalizer
86 def cleanup_pull_request():
86 def cleanup_pull_request():
87 calls = [mock.call(
87 calls = [mock.call(
88 self.pull_request, self.pull_request.author, 'create')]
88 self.pull_request, self.pull_request.author, 'create')]
89 self.hook_mock.assert_has_calls(calls)
89 self.hook_mock.assert_has_calls(calls)
90
90
91 self.workspace_remove_patcher.stop()
91 self.workspace_remove_patcher.stop()
92 self.merge_patcher.stop()
92 self.merge_patcher.stop()
93 self.comment_patcher.stop()
93 self.comment_patcher.stop()
94 self.notification_patcher.stop()
94 self.notification_patcher.stop()
95 self.helper_patcher.stop()
95 self.helper_patcher.stop()
96 self.hook_patcher.stop()
96 self.hook_patcher.stop()
97 self.invalidation_patcher.stop()
97 self.invalidation_patcher.stop()
98
98
99 return self.pull_request
99 return self.pull_request
100
100
101 def test_get_all(self, pull_request):
101 def test_get_all(self, pull_request):
102 prs = PullRequestModel().get_all(pull_request.target_repo)
102 prs = PullRequestModel().get_all(pull_request.target_repo)
103 assert isinstance(prs, list)
103 assert isinstance(prs, list)
104 assert len(prs) == 1
104 assert len(prs) == 1
105
105
106 def test_count_all(self, pull_request):
106 def test_count_all(self, pull_request):
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
108 assert pr_count == 1
108 assert pr_count == 1
109
109
110 def test_get_awaiting_review(self, pull_request):
110 def test_get_awaiting_review(self, pull_request):
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
112 assert isinstance(prs, list)
112 assert isinstance(prs, list)
113 assert len(prs) == 1
113 assert len(prs) == 1
114
114
115 def test_count_awaiting_review(self, pull_request):
115 def test_count_awaiting_review(self, pull_request):
116 pr_count = PullRequestModel().count_awaiting_review(
116 pr_count = PullRequestModel().count_awaiting_review(
117 pull_request.target_repo)
117 pull_request.target_repo)
118 assert pr_count == 1
118 assert pr_count == 1
119
119
120 def test_get_awaiting_my_review(self, pull_request):
120 def test_get_awaiting_my_review(self, pull_request):
121 PullRequestModel().update_reviewers(
121 PullRequestModel().update_reviewers(
122 pull_request, [(pull_request.author, ['author'], False)],
122 pull_request, [(pull_request.author, ['author'], False)],
123 pull_request.author)
123 pull_request.author)
124 prs = PullRequestModel().get_awaiting_my_review(
124 prs = PullRequestModel().get_awaiting_my_review(
125 pull_request.target_repo, user_id=pull_request.author.user_id)
125 pull_request.target_repo, user_id=pull_request.author.user_id)
126 assert isinstance(prs, list)
126 assert isinstance(prs, list)
127 assert len(prs) == 1
127 assert len(prs) == 1
128
128
129 def test_count_awaiting_my_review(self, pull_request):
129 def test_count_awaiting_my_review(self, pull_request):
130 PullRequestModel().update_reviewers(
130 PullRequestModel().update_reviewers(
131 pull_request, [(pull_request.author, ['author'], False)],
131 pull_request, [(pull_request.author, ['author'], False)],
132 pull_request.author)
132 pull_request.author)
133 pr_count = PullRequestModel().count_awaiting_my_review(
133 pr_count = PullRequestModel().count_awaiting_my_review(
134 pull_request.target_repo, user_id=pull_request.author.user_id)
134 pull_request.target_repo, user_id=pull_request.author.user_id)
135 assert pr_count == 1
135 assert pr_count == 1
136
136
137 def test_delete_calls_cleanup_merge(self, pull_request):
137 def test_delete_calls_cleanup_merge(self, pull_request):
138 PullRequestModel().delete(pull_request, pull_request.author)
138 PullRequestModel().delete(pull_request, pull_request.author)
139
139
140 self.workspace_remove_mock.assert_called_once_with(
140 self.workspace_remove_mock.assert_called_once_with(
141 self.workspace_id)
141 self.workspace_id)
142
142
143 def test_close_calls_cleanup_and_hook(self, pull_request):
143 def test_close_calls_cleanup_and_hook(self, pull_request):
144 PullRequestModel().close_pull_request(
144 PullRequestModel().close_pull_request(
145 pull_request, pull_request.author)
145 pull_request, pull_request.author)
146
146
147 self.workspace_remove_mock.assert_called_once_with(
147 self.workspace_remove_mock.assert_called_once_with(
148 self.workspace_id)
148 self.workspace_id)
149 self.hook_mock.assert_called_with(
149 self.hook_mock.assert_called_with(
150 self.pull_request, self.pull_request.author, 'close')
150 self.pull_request, self.pull_request.author, 'close')
151
151
152 def test_merge_status(self, pull_request):
152 def test_merge_status(self, pull_request):
153 self.merge_mock.return_value = MergeResponse(
153 self.merge_mock.return_value = MergeResponse(
154 True, False, None, MergeFailureReason.NONE)
154 True, False, None, MergeFailureReason.NONE)
155
155
156 assert pull_request._last_merge_source_rev is None
156 assert pull_request._last_merge_source_rev is None
157 assert pull_request._last_merge_target_rev is None
157 assert pull_request._last_merge_target_rev is None
158 assert pull_request.last_merge_status is None
158 assert pull_request.last_merge_status is None
159
159
160 status, msg = PullRequestModel().merge_status(pull_request)
160 status, msg = PullRequestModel().merge_status(pull_request)
161 assert status is True
161 assert status is True
162 assert msg.eval() == 'This pull request can be automatically merged.'
162 assert msg.eval() == 'This pull request can be automatically merged.'
163 self.merge_mock.assert_called_once_with(
163 self.merge_mock.assert_called_with(
164 pull_request.target_ref_parts,
164 pull_request.target_ref_parts,
165 pull_request.source_repo.scm_instance(),
165 pull_request.source_repo.scm_instance(),
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
167 use_rebase=False, close_branch=False)
167 use_rebase=False, close_branch=False)
168
168
169 assert pull_request._last_merge_source_rev == self.source_commit
169 assert pull_request._last_merge_source_rev == self.source_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
172
172
173 self.merge_mock.reset_mock()
173 self.merge_mock.reset_mock()
174 status, msg = PullRequestModel().merge_status(pull_request)
174 status, msg = PullRequestModel().merge_status(pull_request)
175 assert status is True
175 assert status is True
176 assert msg.eval() == 'This pull request can be automatically merged.'
176 assert msg.eval() == 'This pull request can be automatically merged.'
177 assert self.merge_mock.called is False
177 assert self.merge_mock.called is False
178
178
179 def test_merge_status_known_failure(self, pull_request):
179 def test_merge_status_known_failure(self, pull_request):
180 self.merge_mock.return_value = MergeResponse(
180 self.merge_mock.return_value = MergeResponse(
181 False, False, None, MergeFailureReason.MERGE_FAILED)
181 False, False, None, MergeFailureReason.MERGE_FAILED)
182
182
183 assert pull_request._last_merge_source_rev is None
183 assert pull_request._last_merge_source_rev is None
184 assert pull_request._last_merge_target_rev is None
184 assert pull_request._last_merge_target_rev is None
185 assert pull_request.last_merge_status is None
185 assert pull_request.last_merge_status is None
186
186
187 status, msg = PullRequestModel().merge_status(pull_request)
187 status, msg = PullRequestModel().merge_status(pull_request)
188 assert status is False
188 assert status is False
189 assert (
189 assert (
190 msg.eval() ==
190 msg.eval() ==
191 'This pull request cannot be merged because of merge conflicts.')
191 'This pull request cannot be merged because of merge conflicts.')
192 self.merge_mock.assert_called_once_with(
192 self.merge_mock.assert_called_with(
193 pull_request.target_ref_parts,
193 pull_request.target_ref_parts,
194 pull_request.source_repo.scm_instance(),
194 pull_request.source_repo.scm_instance(),
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
196 use_rebase=False, close_branch=False)
196 use_rebase=False, close_branch=False)
197
197
198 assert pull_request._last_merge_source_rev == self.source_commit
198 assert pull_request._last_merge_source_rev == self.source_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
200 assert (
200 assert (
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
202
202
203 self.merge_mock.reset_mock()
203 self.merge_mock.reset_mock()
204 status, msg = PullRequestModel().merge_status(pull_request)
204 status, msg = PullRequestModel().merge_status(pull_request)
205 assert status is False
205 assert status is False
206 assert (
206 assert (
207 msg.eval() ==
207 msg.eval() ==
208 'This pull request cannot be merged because of merge conflicts.')
208 'This pull request cannot be merged because of merge conflicts.')
209 assert self.merge_mock.called is False
209 assert self.merge_mock.called is False
210
210
211 def test_merge_status_unknown_failure(self, pull_request):
211 def test_merge_status_unknown_failure(self, pull_request):
212 self.merge_mock.return_value = MergeResponse(
212 self.merge_mock.return_value = MergeResponse(
213 False, False, None, MergeFailureReason.UNKNOWN)
213 False, False, None, MergeFailureReason.UNKNOWN)
214
214
215 assert pull_request._last_merge_source_rev is None
215 assert pull_request._last_merge_source_rev is None
216 assert pull_request._last_merge_target_rev is None
216 assert pull_request._last_merge_target_rev is None
217 assert pull_request.last_merge_status is None
217 assert pull_request.last_merge_status is None
218
218
219 status, msg = PullRequestModel().merge_status(pull_request)
219 status, msg = PullRequestModel().merge_status(pull_request)
220 assert status is False
220 assert status is False
221 assert msg.eval() == (
221 assert msg.eval() == (
222 'This pull request cannot be merged because of an unhandled'
222 'This pull request cannot be merged because of an unhandled'
223 ' exception.')
223 ' exception.')
224 self.merge_mock.assert_called_once_with(
224 self.merge_mock.assert_called_with(
225 pull_request.target_ref_parts,
225 pull_request.target_ref_parts,
226 pull_request.source_repo.scm_instance(),
226 pull_request.source_repo.scm_instance(),
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
228 use_rebase=False, close_branch=False)
228 use_rebase=False, close_branch=False)
229
229
230 assert pull_request._last_merge_source_rev is None
230 assert pull_request._last_merge_source_rev is None
231 assert pull_request._last_merge_target_rev is None
231 assert pull_request._last_merge_target_rev is None
232 assert pull_request.last_merge_status is None
232 assert pull_request.last_merge_status is None
233
233
234 self.merge_mock.reset_mock()
234 self.merge_mock.reset_mock()
235 status, msg = PullRequestModel().merge_status(pull_request)
235 status, msg = PullRequestModel().merge_status(pull_request)
236 assert status is False
236 assert status is False
237 assert msg.eval() == (
237 assert msg.eval() == (
238 'This pull request cannot be merged because of an unhandled'
238 'This pull request cannot be merged because of an unhandled'
239 ' exception.')
239 ' exception.')
240 assert self.merge_mock.called is True
240 assert self.merge_mock.called is True
241
241
242 def test_merge_status_when_target_is_locked(self, pull_request):
242 def test_merge_status_when_target_is_locked(self, pull_request):
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
244 status, msg = PullRequestModel().merge_status(pull_request)
244 status, msg = PullRequestModel().merge_status(pull_request)
245 assert status is False
245 assert status is False
246 assert msg.eval() == (
246 assert msg.eval() == (
247 'This pull request cannot be merged because the target repository'
247 'This pull request cannot be merged because the target repository'
248 ' is locked.')
248 ' is locked.')
249
249
250 def test_merge_status_requirements_check_target(self, pull_request):
250 def test_merge_status_requirements_check_target(self, pull_request):
251
251
252 def has_largefiles(self, repo):
252 def has_largefiles(self, repo):
253 return repo == pull_request.source_repo
253 return repo == pull_request.source_repo
254
254
255 patcher = mock.patch.object(
255 patcher = mock.patch.object(
256 PullRequestModel, '_has_largefiles', has_largefiles)
256 PullRequestModel, '_has_largefiles', has_largefiles)
257 with patcher:
257 with patcher:
258 status, msg = PullRequestModel().merge_status(pull_request)
258 status, msg = PullRequestModel().merge_status(pull_request)
259
259
260 assert status is False
260 assert status is False
261 assert msg == 'Target repository large files support is disabled.'
261 assert msg == 'Target repository large files support is disabled.'
262
262
263 def test_merge_status_requirements_check_source(self, pull_request):
263 def test_merge_status_requirements_check_source(self, pull_request):
264
264
265 def has_largefiles(self, repo):
265 def has_largefiles(self, repo):
266 return repo == pull_request.target_repo
266 return repo == pull_request.target_repo
267
267
268 patcher = mock.patch.object(
268 patcher = mock.patch.object(
269 PullRequestModel, '_has_largefiles', has_largefiles)
269 PullRequestModel, '_has_largefiles', has_largefiles)
270 with patcher:
270 with patcher:
271 status, msg = PullRequestModel().merge_status(pull_request)
271 status, msg = PullRequestModel().merge_status(pull_request)
272
272
273 assert status is False
273 assert status is False
274 assert msg == 'Source repository large files support is disabled.'
274 assert msg == 'Source repository large files support is disabled.'
275
275
276 def test_merge(self, pull_request, merge_extras):
276 def test_merge(self, pull_request, merge_extras):
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
278 merge_ref = Reference(
278 merge_ref = Reference(
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
280 self.merge_mock.return_value = MergeResponse(
280 self.merge_mock.return_value = MergeResponse(
281 True, True, merge_ref, MergeFailureReason.NONE)
281 True, True, merge_ref, MergeFailureReason.NONE)
282
282
283 merge_extras['repository'] = pull_request.target_repo.repo_name
283 merge_extras['repository'] = pull_request.target_repo.repo_name
284 PullRequestModel().merge(
284 PullRequestModel().merge(
285 pull_request, pull_request.author, extras=merge_extras)
285 pull_request, pull_request.author, extras=merge_extras)
286
286
287 message = (
287 message = (
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
289 u'\n\n {pr_title}'.format(
289 u'\n\n {pr_title}'.format(
290 pr_id=pull_request.pull_request_id,
290 pr_id=pull_request.pull_request_id,
291 source_repo=safe_unicode(
291 source_repo=safe_unicode(
292 pull_request.source_repo.scm_instance().name),
292 pull_request.source_repo.scm_instance().name),
293 source_ref_name=pull_request.source_ref_parts.name,
293 source_ref_name=pull_request.source_ref_parts.name,
294 pr_title=safe_unicode(pull_request.title)
294 pr_title=safe_unicode(pull_request.title)
295 )
295 )
296 )
296 )
297 self.merge_mock.assert_called_once_with(
297 self.merge_mock.assert_called_with(
298 pull_request.target_ref_parts,
298 pull_request.target_ref_parts,
299 pull_request.source_repo.scm_instance(),
299 pull_request.source_repo.scm_instance(),
300 pull_request.source_ref_parts, self.workspace_id,
300 pull_request.source_ref_parts, self.workspace_id,
301 user_name=user.username, user_email=user.email, message=message,
301 user_name=user.username, user_email=user.email, message=message,
302 use_rebase=False, close_branch=False
302 use_rebase=False, close_branch=False
303 )
303 )
304 self.invalidation_mock.assert_called_once_with(
304 self.invalidation_mock.assert_called_once_with(
305 pull_request.target_repo.repo_name)
305 pull_request.target_repo.repo_name)
306
306
307 self.hook_mock.assert_called_with(
307 self.hook_mock.assert_called_with(
308 self.pull_request, self.pull_request.author, 'merge')
308 self.pull_request, self.pull_request.author, 'merge')
309
309
310 pull_request = PullRequest.get(pull_request.pull_request_id)
310 pull_request = PullRequest.get(pull_request.pull_request_id)
311 assert (
311 assert (
312 pull_request.merge_rev ==
312 pull_request.merge_rev ==
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314
314
315 def test_merge_failed(self, pull_request, merge_extras):
315 def test_merge_failed(self, pull_request, merge_extras):
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
317 merge_ref = Reference(
317 merge_ref = Reference(
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
319 self.merge_mock.return_value = MergeResponse(
319 self.merge_mock.return_value = MergeResponse(
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
321
321
322 merge_extras['repository'] = pull_request.target_repo.repo_name
322 merge_extras['repository'] = pull_request.target_repo.repo_name
323 PullRequestModel().merge(
323 PullRequestModel().merge(
324 pull_request, pull_request.author, extras=merge_extras)
324 pull_request, pull_request.author, extras=merge_extras)
325
325
326 message = (
326 message = (
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
328 u'\n\n {pr_title}'.format(
328 u'\n\n {pr_title}'.format(
329 pr_id=pull_request.pull_request_id,
329 pr_id=pull_request.pull_request_id,
330 source_repo=safe_unicode(
330 source_repo=safe_unicode(
331 pull_request.source_repo.scm_instance().name),
331 pull_request.source_repo.scm_instance().name),
332 source_ref_name=pull_request.source_ref_parts.name,
332 source_ref_name=pull_request.source_ref_parts.name,
333 pr_title=safe_unicode(pull_request.title)
333 pr_title=safe_unicode(pull_request.title)
334 )
334 )
335 )
335 )
336 self.merge_mock.assert_called_once_with(
336 self.merge_mock.assert_called_with(
337 pull_request.target_ref_parts,
337 pull_request.target_ref_parts,
338 pull_request.source_repo.scm_instance(),
338 pull_request.source_repo.scm_instance(),
339 pull_request.source_ref_parts, self.workspace_id,
339 pull_request.source_ref_parts, self.workspace_id,
340 user_name=user.username, user_email=user.email, message=message,
340 user_name=user.username, user_email=user.email, message=message,
341 use_rebase=False, close_branch=False
341 use_rebase=False, close_branch=False
342 )
342 )
343
343
344 pull_request = PullRequest.get(pull_request.pull_request_id)
344 pull_request = PullRequest.get(pull_request.pull_request_id)
345 assert self.invalidation_mock.called is False
345 assert self.invalidation_mock.called is False
346 assert pull_request.merge_rev is None
346 assert pull_request.merge_rev is None
347
347
348 def test_get_commit_ids(self, pull_request):
348 def test_get_commit_ids(self, pull_request):
349 # The PR has been not merget yet, so expect an exception
349 # The PR has been not merget yet, so expect an exception
350 with pytest.raises(ValueError):
350 with pytest.raises(ValueError):
351 PullRequestModel()._get_commit_ids(pull_request)
351 PullRequestModel()._get_commit_ids(pull_request)
352
352
353 # Merge revision is in the revisions list
353 # Merge revision is in the revisions list
354 pull_request.merge_rev = pull_request.revisions[0]
354 pull_request.merge_rev = pull_request.revisions[0]
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions
356 assert commit_ids == pull_request.revisions
357
357
358 # Merge revision is not in the revisions list
358 # Merge revision is not in the revisions list
359 pull_request.merge_rev = 'f000' * 10
359 pull_request.merge_rev = 'f000' * 10
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
362
362
363 def test_get_diff_from_pr_version(self, pull_request):
363 def test_get_diff_from_pr_version(self, pull_request):
364 source_repo = pull_request.source_repo
364 source_repo = pull_request.source_repo
365 source_ref_id = pull_request.source_ref_parts.commit_id
365 source_ref_id = pull_request.source_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
368 source_repo, source_ref_id, target_ref_id, context=6)
368 source_repo, source_ref_id, target_ref_id, context=6)
369 assert 'file_1' in diff.raw
369 assert 'file_1' in diff.raw
370
370
371 def test_generate_title_returns_unicode(self):
371 def test_generate_title_returns_unicode(self):
372 title = PullRequestModel().generate_pullrequest_title(
372 title = PullRequestModel().generate_pullrequest_title(
373 source='source-dummy',
373 source='source-dummy',
374 source_ref='source-ref-dummy',
374 source_ref='source-ref-dummy',
375 target='target-dummy',
375 target='target-dummy',
376 )
376 )
377 assert type(title) == unicode
377 assert type(title) == unicode
378
378
379
379
380 @pytest.mark.usefixtures('config_stub')
380 @pytest.mark.usefixtures('config_stub')
381 class TestIntegrationMerge(object):
381 class TestIntegrationMerge(object):
382 @pytest.mark.parametrize('extra_config', (
382 @pytest.mark.parametrize('extra_config', (
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
384 ))
384 ))
385 def test_merge_triggers_push_hooks(
385 def test_merge_triggers_push_hooks(
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
387 extra_config):
387 extra_config):
388 pull_request = pr_util.create_pull_request(
388 pull_request = pr_util.create_pull_request(
389 approved=True, mergeable=True)
389 approved=True, mergeable=True)
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
391 merge_extras['repository'] = pull_request.target_repo.repo_name
391 merge_extras['repository'] = pull_request.target_repo.repo_name
392 Session().commit()
392 Session().commit()
393
393
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
395 merge_state = PullRequestModel().merge(
395 merge_state = PullRequestModel().merge(
396 pull_request, user_admin, extras=merge_extras)
396 pull_request, user_admin, extras=merge_extras)
397
397
398 assert merge_state.executed
398 assert merge_state.executed
399 assert 'pre_push' in capture_rcextensions
399 assert 'pre_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
401
401
402 def test_merge_can_be_rejected_by_pre_push_hook(
402 def test_merge_can_be_rejected_by_pre_push_hook(
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
404 pull_request = pr_util.create_pull_request(
404 pull_request = pr_util.create_pull_request(
405 approved=True, mergeable=True)
405 approved=True, mergeable=True)
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
407 merge_extras['repository'] = pull_request.target_repo.repo_name
407 merge_extras['repository'] = pull_request.target_repo.repo_name
408 Session().commit()
408 Session().commit()
409
409
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
411 pre_pull.side_effect = RepositoryError("Disallow push!")
411 pre_pull.side_effect = RepositoryError("Disallow push!")
412 merge_status = PullRequestModel().merge(
412 merge_status = PullRequestModel().merge(
413 pull_request, user_admin, extras=merge_extras)
413 pull_request, user_admin, extras=merge_extras)
414
414
415 assert not merge_status.executed
415 assert not merge_status.executed
416 assert 'pre_push' not in capture_rcextensions
416 assert 'pre_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
418
418
419 def test_merge_fails_if_target_is_locked(
419 def test_merge_fails_if_target_is_locked(
420 self, pr_util, user_regular, merge_extras):
420 self, pr_util, user_regular, merge_extras):
421 pull_request = pr_util.create_pull_request(
421 pull_request = pr_util.create_pull_request(
422 approved=True, mergeable=True)
422 approved=True, mergeable=True)
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
424 pull_request.target_repo.locked = locked_by
424 pull_request.target_repo.locked = locked_by
425 # TODO: johbo: Check if this can work based on the database, currently
425 # TODO: johbo: Check if this can work based on the database, currently
426 # all data is pre-computed, that's why just updating the DB is not
426 # all data is pre-computed, that's why just updating the DB is not
427 # enough.
427 # enough.
428 merge_extras['locked_by'] = locked_by
428 merge_extras['locked_by'] = locked_by
429 merge_extras['repository'] = pull_request.target_repo.repo_name
429 merge_extras['repository'] = pull_request.target_repo.repo_name
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
431 Session().commit()
431 Session().commit()
432 merge_status = PullRequestModel().merge(
432 merge_status = PullRequestModel().merge(
433 pull_request, user_regular, extras=merge_extras)
433 pull_request, user_regular, extras=merge_extras)
434 assert not merge_status.executed
434 assert not merge_status.executed
435
435
436
436
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
438 (False, 1, 0),
438 (False, 1, 0),
439 (True, 0, 1),
439 (True, 0, 1),
440 ])
440 ])
441 def test_outdated_comments(
441 def test_outdated_comments(
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
443 pull_request = pr_util.create_pull_request()
443 pull_request = pr_util.create_pull_request()
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
445
445
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
447 pr_util.add_one_commit()
447 pr_util.add_one_commit()
448 assert_inline_comments(
448 assert_inline_comments(
449 pull_request, visible=inlines_count, outdated=outdated_count)
449 pull_request, visible=inlines_count, outdated=outdated_count)
450 outdated_comment_mock.assert_called_with(pull_request)
450 outdated_comment_mock.assert_called_with(pull_request)
451
451
452
452
453 @pytest.fixture
453 @pytest.fixture
454 def merge_extras(user_regular):
454 def merge_extras(user_regular):
455 """
455 """
456 Context for the vcs operation when running a merge.
456 Context for the vcs operation when running a merge.
457 """
457 """
458 extras = {
458 extras = {
459 'ip': '127.0.0.1',
459 'ip': '127.0.0.1',
460 'username': user_regular.username,
460 'username': user_regular.username,
461 'action': 'push',
461 'action': 'push',
462 'repository': 'fake_target_repo_name',
462 'repository': 'fake_target_repo_name',
463 'scm': 'git',
463 'scm': 'git',
464 'config': 'fake_config_ini_path',
464 'config': 'fake_config_ini_path',
465 'make_lock': None,
465 'make_lock': None,
466 'locked_by': [None, None, None],
466 'locked_by': [None, None, None],
467 'server_url': 'http://test.example.com:5000',
467 'server_url': 'http://test.example.com:5000',
468 'hooks': ['push', 'pull'],
468 'hooks': ['push', 'pull'],
469 'is_shadow_repo': False,
469 'is_shadow_repo': False,
470 }
470 }
471 return extras
471 return extras
472
472
473
473
474 @pytest.mark.usefixtures('config_stub')
474 @pytest.mark.usefixtures('config_stub')
475 class TestUpdateCommentHandling(object):
475 class TestUpdateCommentHandling(object):
476
476
477 @pytest.fixture(autouse=True, scope='class')
477 @pytest.fixture(autouse=True, scope='class')
478 def enable_outdated_comments(self, request, pylonsapp):
478 def enable_outdated_comments(self, request, pylonsapp):
479 config_patch = mock.patch.dict(
479 config_patch = mock.patch.dict(
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
481 config_patch.start()
481 config_patch.start()
482
482
483 @request.addfinalizer
483 @request.addfinalizer
484 def cleanup():
484 def cleanup():
485 config_patch.stop()
485 config_patch.stop()
486
486
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
488 commits = [
488 commits = [
489 {'message': 'a'},
489 {'message': 'a'},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
492 ]
492 ]
493 pull_request = pr_util.create_pull_request(
493 pull_request = pr_util.create_pull_request(
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
495 pr_util.create_inline_comment(file_path='file_b')
495 pr_util.create_inline_comment(file_path='file_b')
496 pr_util.add_one_commit(head='c')
496 pr_util.add_one_commit(head='c')
497
497
498 assert_inline_comments(pull_request, visible=1, outdated=0)
498 assert_inline_comments(pull_request, visible=1, outdated=0)
499
499
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
501 original_content = ''.join(
501 original_content = ''.join(
502 ['line {}\n'.format(x) for x in range(1, 11)])
502 ['line {}\n'.format(x) for x in range(1, 11)])
503 updated_content = 'new_line_at_top\n' + original_content
503 updated_content = 'new_line_at_top\n' + original_content
504 commits = [
504 commits = [
505 {'message': 'a'},
505 {'message': 'a'},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
508 ]
508 ]
509 pull_request = pr_util.create_pull_request(
509 pull_request = pr_util.create_pull_request(
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
511
511
512 with outdated_comments_patcher():
512 with outdated_comments_patcher():
513 comment = pr_util.create_inline_comment(
513 comment = pr_util.create_inline_comment(
514 line_no=u'n8', file_path='file_b')
514 line_no=u'n8', file_path='file_b')
515 pr_util.add_one_commit(head='c')
515 pr_util.add_one_commit(head='c')
516
516
517 assert_inline_comments(pull_request, visible=1, outdated=0)
517 assert_inline_comments(pull_request, visible=1, outdated=0)
518 assert comment.line_no == u'n9'
518 assert comment.line_no == u'n9'
519
519
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
522 updated_content = original_content + 'new_line_at_end\n'
522 updated_content = original_content + 'new_line_at_end\n'
523 commits = [
523 commits = [
524 {'message': 'a'},
524 {'message': 'a'},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
527 ]
527 ]
528 pull_request = pr_util.create_pull_request(
528 pull_request = pr_util.create_pull_request(
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
530 pr_util.create_inline_comment(file_path='file_b')
530 pr_util.create_inline_comment(file_path='file_b')
531 pr_util.add_one_commit(head='c')
531 pr_util.add_one_commit(head='c')
532
532
533 assert_inline_comments(pull_request, visible=1, outdated=0)
533 assert_inline_comments(pull_request, visible=1, outdated=0)
534
534
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
538 change_lines = list(base_lines)
538 change_lines = list(base_lines)
539 change_lines.insert(6, 'line 6a added\n')
539 change_lines.insert(6, 'line 6a added\n')
540
540
541 # Changes on the last line of sight
541 # Changes on the last line of sight
542 update_lines = list(change_lines)
542 update_lines = list(change_lines)
543 update_lines[0] = 'line 1 changed\n'
543 update_lines[0] = 'line 1 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
545
545
546 def file_b(lines):
546 def file_b(lines):
547 return FileNode('file_b', ''.join(lines))
547 return FileNode('file_b', ''.join(lines))
548
548
549 commits = [
549 commits = [
550 {'message': 'a', 'added': [file_b(base_lines)]},
550 {'message': 'a', 'added': [file_b(base_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
553 ]
553 ]
554
554
555 pull_request = pr_util.create_pull_request(
555 pull_request = pr_util.create_pull_request(
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
558
558
559 with outdated_comments_patcher():
559 with outdated_comments_patcher():
560 pr_util.add_one_commit(head='c')
560 pr_util.add_one_commit(head='c')
561 assert_inline_comments(pull_request, visible=0, outdated=1)
561 assert_inline_comments(pull_request, visible=0, outdated=1)
562
562
563 @pytest.mark.parametrize("change, content", [
563 @pytest.mark.parametrize("change, content", [
564 ('changed', 'changed\n'),
564 ('changed', 'changed\n'),
565 ('removed', ''),
565 ('removed', ''),
566 ], ids=['changed', 'removed'])
566 ], ids=['changed', 'removed'])
567 def test_comment_flagged_on_change(self, pr_util, change, content):
567 def test_comment_flagged_on_change(self, pr_util, change, content):
568 commits = [
568 commits = [
569 {'message': 'a'},
569 {'message': 'a'},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
572 ]
572 ]
573 pull_request = pr_util.create_pull_request(
573 pull_request = pr_util.create_pull_request(
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
575 pr_util.create_inline_comment(file_path='file_b')
575 pr_util.create_inline_comment(file_path='file_b')
576
576
577 with outdated_comments_patcher():
577 with outdated_comments_patcher():
578 pr_util.add_one_commit(head='c')
578 pr_util.add_one_commit(head='c')
579 assert_inline_comments(pull_request, visible=0, outdated=1)
579 assert_inline_comments(pull_request, visible=0, outdated=1)
580
580
581
581
582 @pytest.mark.usefixtures('config_stub')
582 @pytest.mark.usefixtures('config_stub')
583 class TestUpdateChangedFiles(object):
583 class TestUpdateChangedFiles(object):
584
584
585 def test_no_changes_on_unchanged_diff(self, pr_util):
585 def test_no_changes_on_unchanged_diff(self, pr_util):
586 commits = [
586 commits = [
587 {'message': 'a'},
587 {'message': 'a'},
588 {'message': 'b',
588 {'message': 'b',
589 'added': [FileNode('file_b', 'test_content b\n')]},
589 'added': [FileNode('file_b', 'test_content b\n')]},
590 {'message': 'c',
590 {'message': 'c',
591 'added': [FileNode('file_c', 'test_content c\n')]},
591 'added': [FileNode('file_c', 'test_content c\n')]},
592 ]
592 ]
593 # open a PR from a to b, adding file_b
593 # open a PR from a to b, adding file_b
594 pull_request = pr_util.create_pull_request(
594 pull_request = pr_util.create_pull_request(
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
596 name_suffix='per-file-review')
596 name_suffix='per-file-review')
597
597
598 # modify PR adding new file file_c
598 # modify PR adding new file file_c
599 pr_util.add_one_commit(head='c')
599 pr_util.add_one_commit(head='c')
600
600
601 assert_pr_file_changes(
601 assert_pr_file_changes(
602 pull_request,
602 pull_request,
603 added=['file_c'],
603 added=['file_c'],
604 modified=[],
604 modified=[],
605 removed=[])
605 removed=[])
606
606
607 def test_modify_and_undo_modification_diff(self, pr_util):
607 def test_modify_and_undo_modification_diff(self, pr_util):
608 commits = [
608 commits = [
609 {'message': 'a'},
609 {'message': 'a'},
610 {'message': 'b',
610 {'message': 'b',
611 'added': [FileNode('file_b', 'test_content b\n')]},
611 'added': [FileNode('file_b', 'test_content b\n')]},
612 {'message': 'c',
612 {'message': 'c',
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
614 {'message': 'd',
614 {'message': 'd',
615 'changed': [FileNode('file_b', 'test_content b\n')]},
615 'changed': [FileNode('file_b', 'test_content b\n')]},
616 ]
616 ]
617 # open a PR from a to b, adding file_b
617 # open a PR from a to b, adding file_b
618 pull_request = pr_util.create_pull_request(
618 pull_request = pr_util.create_pull_request(
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
620 name_suffix='per-file-review')
620 name_suffix='per-file-review')
621
621
622 # modify PR modifying file file_b
622 # modify PR modifying file file_b
623 pr_util.add_one_commit(head='c')
623 pr_util.add_one_commit(head='c')
624
624
625 assert_pr_file_changes(
625 assert_pr_file_changes(
626 pull_request,
626 pull_request,
627 added=[],
627 added=[],
628 modified=['file_b'],
628 modified=['file_b'],
629 removed=[])
629 removed=[])
630
630
631 # move the head again to d, which rollbacks change,
631 # move the head again to d, which rollbacks change,
632 # meaning we should indicate no changes
632 # meaning we should indicate no changes
633 pr_util.add_one_commit(head='d')
633 pr_util.add_one_commit(head='d')
634
634
635 assert_pr_file_changes(
635 assert_pr_file_changes(
636 pull_request,
636 pull_request,
637 added=[],
637 added=[],
638 modified=[],
638 modified=[],
639 removed=[])
639 removed=[])
640
640
641 def test_updated_all_files_in_pr(self, pr_util):
641 def test_updated_all_files_in_pr(self, pr_util):
642 commits = [
642 commits = [
643 {'message': 'a'},
643 {'message': 'a'},
644 {'message': 'b', 'added': [
644 {'message': 'b', 'added': [
645 FileNode('file_a', 'test_content a\n'),
645 FileNode('file_a', 'test_content a\n'),
646 FileNode('file_b', 'test_content b\n'),
646 FileNode('file_b', 'test_content b\n'),
647 FileNode('file_c', 'test_content c\n')]},
647 FileNode('file_c', 'test_content c\n')]},
648 {'message': 'c', 'changed': [
648 {'message': 'c', 'changed': [
649 FileNode('file_a', 'test_content a changed\n'),
649 FileNode('file_a', 'test_content a changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
651 FileNode('file_c', 'test_content c changed\n')]},
651 FileNode('file_c', 'test_content c changed\n')]},
652 ]
652 ]
653 # open a PR from a to b, changing 3 files
653 # open a PR from a to b, changing 3 files
654 pull_request = pr_util.create_pull_request(
654 pull_request = pr_util.create_pull_request(
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
656 name_suffix='per-file-review')
656 name_suffix='per-file-review')
657
657
658 pr_util.add_one_commit(head='c')
658 pr_util.add_one_commit(head='c')
659
659
660 assert_pr_file_changes(
660 assert_pr_file_changes(
661 pull_request,
661 pull_request,
662 added=[],
662 added=[],
663 modified=['file_a', 'file_b', 'file_c'],
663 modified=['file_a', 'file_b', 'file_c'],
664 removed=[])
664 removed=[])
665
665
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
667 commits = [
667 commits = [
668 {'message': 'a'},
668 {'message': 'a'},
669 {'message': 'b', 'added': [
669 {'message': 'b', 'added': [
670 FileNode('file_a', 'test_content a\n'),
670 FileNode('file_a', 'test_content a\n'),
671 FileNode('file_b', 'test_content b\n'),
671 FileNode('file_b', 'test_content b\n'),
672 FileNode('file_c', 'test_content c\n')]},
672 FileNode('file_c', 'test_content c\n')]},
673 {'message': 'c', 'removed': [
673 {'message': 'c', 'removed': [
674 FileNode('file_a', 'test_content a changed\n'),
674 FileNode('file_a', 'test_content a changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
676 FileNode('file_c', 'test_content c changed\n')]},
676 FileNode('file_c', 'test_content c changed\n')]},
677 ]
677 ]
678 # open a PR from a to b, removing 3 files
678 # open a PR from a to b, removing 3 files
679 pull_request = pr_util.create_pull_request(
679 pull_request = pr_util.create_pull_request(
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
681 name_suffix='per-file-review')
681 name_suffix='per-file-review')
682
682
683 pr_util.add_one_commit(head='c')
683 pr_util.add_one_commit(head='c')
684
684
685 assert_pr_file_changes(
685 assert_pr_file_changes(
686 pull_request,
686 pull_request,
687 added=[],
687 added=[],
688 modified=[],
688 modified=[],
689 removed=['file_a', 'file_b', 'file_c'])
689 removed=['file_a', 'file_b', 'file_c'])
690
690
691
691
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
693 model = PullRequestModel()
693 model = PullRequestModel()
694 pull_request = pr_util.create_pull_request()
694 pull_request = pr_util.create_pull_request()
695 pr_util.update_source_repository()
695 pr_util.update_source_repository()
696
696
697 model.update_commits(pull_request)
697 model.update_commits(pull_request)
698
698
699 # Expect that it has a version entry now
699 # Expect that it has a version entry now
700 assert len(model.get_versions(pull_request)) == 1
700 assert len(model.get_versions(pull_request)) == 1
701
701
702
702
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
704 pull_request = pr_util.create_pull_request()
704 pull_request = pr_util.create_pull_request()
705 model = PullRequestModel()
705 model = PullRequestModel()
706 model.update_commits(pull_request)
706 model.update_commits(pull_request)
707
707
708 # Expect that it still has no versions
708 # Expect that it still has no versions
709 assert len(model.get_versions(pull_request)) == 0
709 assert len(model.get_versions(pull_request)) == 0
710
710
711
711
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
713 model = PullRequestModel()
713 model = PullRequestModel()
714 pull_request = pr_util.create_pull_request()
714 pull_request = pr_util.create_pull_request()
715 comment = pr_util.create_comment()
715 comment = pr_util.create_comment()
716 pr_util.update_source_repository()
716 pr_util.update_source_repository()
717
717
718 model.update_commits(pull_request)
718 model.update_commits(pull_request)
719
719
720 # Expect that the comment is linked to the pr version now
720 # Expect that the comment is linked to the pr version now
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
722
722
723
723
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
725 model = PullRequestModel()
725 model = PullRequestModel()
726 pull_request = pr_util.create_pull_request()
726 pull_request = pr_util.create_pull_request()
727 pr_util.update_source_repository()
727 pr_util.update_source_repository()
728 pr_util.update_source_repository()
728 pr_util.update_source_repository()
729
729
730 model.update_commits(pull_request)
730 model.update_commits(pull_request)
731
731
732 # Expect to find a new comment about the change
732 # Expect to find a new comment about the change
733 expected_message = textwrap.dedent(
733 expected_message = textwrap.dedent(
734 """\
734 """\
735 Pull request updated. Auto status change to |under_review|
735 Pull request updated. Auto status change to |under_review|
736
736
737 .. role:: added
737 .. role:: added
738 .. role:: removed
738 .. role:: removed
739 .. parsed-literal::
739 .. parsed-literal::
740
740
741 Changed commits:
741 Changed commits:
742 * :added:`1 added`
742 * :added:`1 added`
743 * :removed:`0 removed`
743 * :removed:`0 removed`
744
744
745 Changed files:
745 Changed files:
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
747
747
748 .. |under_review| replace:: *"Under Review"*"""
748 .. |under_review| replace:: *"Under Review"*"""
749 )
749 )
750 pull_request_comments = sorted(
750 pull_request_comments = sorted(
751 pull_request.comments, key=lambda c: c.modified_at)
751 pull_request.comments, key=lambda c: c.modified_at)
752 update_comment = pull_request_comments[-1]
752 update_comment = pull_request_comments[-1]
753 assert update_comment.text == expected_message
753 assert update_comment.text == expected_message
754
754
755
755
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
757 pull_request = pr_util.create_pull_request()
757 pull_request = pr_util.create_pull_request()
758
758
759 # Avoiding default values
759 # Avoiding default values
760 pull_request.status = PullRequest.STATUS_CLOSED
760 pull_request.status = PullRequest.STATUS_CLOSED
761 pull_request._last_merge_source_rev = "0" * 40
761 pull_request._last_merge_source_rev = "0" * 40
762 pull_request._last_merge_target_rev = "1" * 40
762 pull_request._last_merge_target_rev = "1" * 40
763 pull_request.last_merge_status = 1
763 pull_request.last_merge_status = 1
764 pull_request.merge_rev = "2" * 40
764 pull_request.merge_rev = "2" * 40
765
765
766 # Remember automatic values
766 # Remember automatic values
767 created_on = pull_request.created_on
767 created_on = pull_request.created_on
768 updated_on = pull_request.updated_on
768 updated_on = pull_request.updated_on
769
769
770 # Create a new version of the pull request
770 # Create a new version of the pull request
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
772
772
773 # Check attributes
773 # Check attributes
774 assert version.title == pr_util.create_parameters['title']
774 assert version.title == pr_util.create_parameters['title']
775 assert version.description == pr_util.create_parameters['description']
775 assert version.description == pr_util.create_parameters['description']
776 assert version.status == PullRequest.STATUS_CLOSED
776 assert version.status == PullRequest.STATUS_CLOSED
777
777
778 # versions get updated created_on
778 # versions get updated created_on
779 assert version.created_on != created_on
779 assert version.created_on != created_on
780
780
781 assert version.updated_on == updated_on
781 assert version.updated_on == updated_on
782 assert version.user_id == pull_request.user_id
782 assert version.user_id == pull_request.user_id
783 assert version.revisions == pr_util.create_parameters['revisions']
783 assert version.revisions == pr_util.create_parameters['revisions']
784 assert version.source_repo == pr_util.source_repository
784 assert version.source_repo == pr_util.source_repository
785 assert version.source_ref == pr_util.create_parameters['source_ref']
785 assert version.source_ref == pr_util.create_parameters['source_ref']
786 assert version.target_repo == pr_util.target_repository
786 assert version.target_repo == pr_util.target_repository
787 assert version.target_ref == pr_util.create_parameters['target_ref']
787 assert version.target_ref == pr_util.create_parameters['target_ref']
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
790 assert version.last_merge_status == pull_request.last_merge_status
790 assert version.last_merge_status == pull_request.last_merge_status
791 assert version.merge_rev == pull_request.merge_rev
791 assert version.merge_rev == pull_request.merge_rev
792 assert version.pull_request == pull_request
792 assert version.pull_request == pull_request
793
793
794
794
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
796 version1 = pr_util.create_version_of_pull_request()
796 version1 = pr_util.create_version_of_pull_request()
797 comment_linked = pr_util.create_comment(linked_to=version1)
797 comment_linked = pr_util.create_comment(linked_to=version1)
798 comment_unlinked = pr_util.create_comment()
798 comment_unlinked = pr_util.create_comment()
799 version2 = pr_util.create_version_of_pull_request()
799 version2 = pr_util.create_version_of_pull_request()
800
800
801 PullRequestModel()._link_comments_to_version(version2)
801 PullRequestModel()._link_comments_to_version(version2)
802
802
803 # Expect that only the new comment is linked to version2
803 # Expect that only the new comment is linked to version2
804 assert (
804 assert (
805 comment_unlinked.pull_request_version_id ==
805 comment_unlinked.pull_request_version_id ==
806 version2.pull_request_version_id)
806 version2.pull_request_version_id)
807 assert (
807 assert (
808 comment_linked.pull_request_version_id ==
808 comment_linked.pull_request_version_id ==
809 version1.pull_request_version_id)
809 version1.pull_request_version_id)
810 assert (
810 assert (
811 comment_unlinked.pull_request_version_id !=
811 comment_unlinked.pull_request_version_id !=
812 comment_linked.pull_request_version_id)
812 comment_linked.pull_request_version_id)
813
813
814
814
815 def test_calculate_commits():
815 def test_calculate_commits():
816 old_ids = [1, 2, 3]
816 old_ids = [1, 2, 3]
817 new_ids = [1, 3, 4, 5]
817 new_ids = [1, 3, 4, 5]
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
819 assert change.added == [4, 5]
819 assert change.added == [4, 5]
820 assert change.common == [1, 3]
820 assert change.common == [1, 3]
821 assert change.removed == [2]
821 assert change.removed == [2]
822 assert change.total == [1, 3, 4, 5]
822 assert change.total == [1, 3, 4, 5]
823
823
824
824
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
826 if visible is not None:
826 if visible is not None:
827 inline_comments = CommentsModel().get_inline_comments(
827 inline_comments = CommentsModel().get_inline_comments(
828 pull_request.target_repo.repo_id, pull_request=pull_request)
828 pull_request.target_repo.repo_id, pull_request=pull_request)
829 inline_cnt = CommentsModel().get_inline_comments_count(
829 inline_cnt = CommentsModel().get_inline_comments_count(
830 inline_comments)
830 inline_comments)
831 assert inline_cnt == visible
831 assert inline_cnt == visible
832 if outdated is not None:
832 if outdated is not None:
833 outdated_comments = CommentsModel().get_outdated_comments(
833 outdated_comments = CommentsModel().get_outdated_comments(
834 pull_request.target_repo.repo_id, pull_request)
834 pull_request.target_repo.repo_id, pull_request)
835 assert len(outdated_comments) == outdated
835 assert len(outdated_comments) == outdated
836
836
837
837
838 def assert_pr_file_changes(
838 def assert_pr_file_changes(
839 pull_request, added=None, modified=None, removed=None):
839 pull_request, added=None, modified=None, removed=None):
840 pr_versions = PullRequestModel().get_versions(pull_request)
840 pr_versions = PullRequestModel().get_versions(pull_request)
841 # always use first version, ie original PR to calculate changes
841 # always use first version, ie original PR to calculate changes
842 pull_request_version = pr_versions[0]
842 pull_request_version = pr_versions[0]
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
844 pull_request, pull_request_version)
844 pull_request, pull_request_version)
845 file_changes = PullRequestModel()._calculate_file_changes(
845 file_changes = PullRequestModel()._calculate_file_changes(
846 old_diff_data, new_diff_data)
846 old_diff_data, new_diff_data)
847
847
848 assert added == file_changes.added, \
848 assert added == file_changes.added, \
849 'expected added:%s vs value:%s' % (added, file_changes.added)
849 'expected added:%s vs value:%s' % (added, file_changes.added)
850 assert modified == file_changes.modified, \
850 assert modified == file_changes.modified, \
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
852 assert removed == file_changes.removed, \
852 assert removed == file_changes.removed, \
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
854
854
855
855
856 def outdated_comments_patcher(use_outdated=True):
856 def outdated_comments_patcher(use_outdated=True):
857 return mock.patch.object(
857 return mock.patch.object(
858 CommentsModel, 'use_outdated_comments',
858 CommentsModel, 'use_outdated_comments',
859 return_value=use_outdated)
859 return_value=use_outdated)
@@ -1,43 +1,55 b''
1 # Copyright (C) 2016-2017 RhodeCode GmbH
1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 from pyramid.i18n import TranslationStringFactory, TranslationString
19 from pyramid.i18n import TranslationStringFactory, TranslationString
20
20
21 # Create a translation string factory for the 'rhodecode' domain.
21 # Create a translation string factory for the 'rhodecode' domain.
22 _ = TranslationStringFactory('rhodecode')
22 _ = TranslationStringFactory('rhodecode')
23
23
24
24
25 class LazyString(object):
25 class LazyString(object):
26 def __init__(self, *args, **kw):
26 def __init__(self, *args, **kw):
27 self.args = args
27 self.args = args
28 self.kw = kw
28 self.kw = kw
29
29
30 def eval(self):
31 return _(*self.args, **self.kw)
32
33 def __unicode__(self):
34 return unicode(self.eval())
35
30 def __str__(self):
36 def __str__(self):
31 return _(*self.args, **self.kw)
37 return self.eval()
38
39 def __mod__(self, other):
40 return self.eval() % other
41
42 def format(self, *args):
43 return self.eval().format(*args)
32
44
33
45
34 def lazy_ugettext(*args, **kw):
46 def lazy_ugettext(*args, **kw):
35 """ Lazily evaluated version of _() """
47 """ Lazily evaluated version of _() """
36 return LazyString(*args, **kw)
48 return LazyString(*args, **kw)
37
49
38
50
39 def _pluralize(msgid1, msgid2, n, mapping=None):
51 def _pluralize(msgid1, msgid2, n, mapping=None):
40 if n == 1:
52 if n == 1:
41 return _(msgid1, mapping=mapping)
53 return _(msgid1, mapping=mapping)
42 else:
54 else:
43 return _(msgid2, mapping=mapping)
55 return _(msgid2, mapping=mapping)
General Comments 0
You need to be logged in to leave comments. Login now