##// END OF EJS Templates
pull-requests: unified merge checks....
marcink -
r1335:7ea0471c default
parent child Browse files
Show More
@@ -1,714 +1,715 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.api import jsonrpc_method, JSONRPCError
24 from rhodecode.api import jsonrpc_method, JSONRPCError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error)
28 validate_repo_permissions, resolve_ref_or_error)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus
34 from rhodecode.model.db import Session, ChangesetStatus
35 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 @jsonrpc_method()
41 @jsonrpc_method()
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 """
43 """
44 Get a pull request based on the given ID.
44 Get a pull request based on the given ID.
45
45
46 :param apiuser: This is filled automatically from the |authtoken|.
46 :param apiuser: This is filled automatically from the |authtoken|.
47 :type apiuser: AuthUser
47 :type apiuser: AuthUser
48 :param repoid: Repository name or repository ID from where the pull
48 :param repoid: Repository name or repository ID from where the pull
49 request was opened.
49 request was opened.
50 :type repoid: str or int
50 :type repoid: str or int
51 :param pullrequestid: ID of the requested pull request.
51 :param pullrequestid: ID of the requested pull request.
52 :type pullrequestid: int
52 :type pullrequestid: int
53
53
54 Example output:
54 Example output:
55
55
56 .. code-block:: bash
56 .. code-block:: bash
57
57
58 "id": <id_given_in_input>,
58 "id": <id_given_in_input>,
59 "result":
59 "result":
60 {
60 {
61 "pull_request_id": "<pull_request_id>",
61 "pull_request_id": "<pull_request_id>",
62 "url": "<url>",
62 "url": "<url>",
63 "title": "<title>",
63 "title": "<title>",
64 "description": "<description>",
64 "description": "<description>",
65 "status" : "<status>",
65 "status" : "<status>",
66 "created_on": "<date_time_created>",
66 "created_on": "<date_time_created>",
67 "updated_on": "<date_time_updated>",
67 "updated_on": "<date_time_updated>",
68 "commit_ids": [
68 "commit_ids": [
69 ...
69 ...
70 "<commit_id>",
70 "<commit_id>",
71 "<commit_id>",
71 "<commit_id>",
72 ...
72 ...
73 ],
73 ],
74 "review_status": "<review_status>",
74 "review_status": "<review_status>",
75 "mergeable": {
75 "mergeable": {
76 "status": "<bool>",
76 "status": "<bool>",
77 "message": "<message>",
77 "message": "<message>",
78 },
78 },
79 "source": {
79 "source": {
80 "clone_url": "<clone_url>",
80 "clone_url": "<clone_url>",
81 "repository": "<repository_name>",
81 "repository": "<repository_name>",
82 "reference":
82 "reference":
83 {
83 {
84 "name": "<name>",
84 "name": "<name>",
85 "type": "<type>",
85 "type": "<type>",
86 "commit_id": "<commit_id>",
86 "commit_id": "<commit_id>",
87 }
87 }
88 },
88 },
89 "target": {
89 "target": {
90 "clone_url": "<clone_url>",
90 "clone_url": "<clone_url>",
91 "repository": "<repository_name>",
91 "repository": "<repository_name>",
92 "reference":
92 "reference":
93 {
93 {
94 "name": "<name>",
94 "name": "<name>",
95 "type": "<type>",
95 "type": "<type>",
96 "commit_id": "<commit_id>",
96 "commit_id": "<commit_id>",
97 }
97 }
98 },
98 },
99 "merge": {
99 "merge": {
100 "clone_url": "<clone_url>",
100 "clone_url": "<clone_url>",
101 "reference":
101 "reference":
102 {
102 {
103 "name": "<name>",
103 "name": "<name>",
104 "type": "<type>",
104 "type": "<type>",
105 "commit_id": "<commit_id>",
105 "commit_id": "<commit_id>",
106 }
106 }
107 },
107 },
108 "author": <user_obj>,
108 "author": <user_obj>,
109 "reviewers": [
109 "reviewers": [
110 ...
110 ...
111 {
111 {
112 "user": "<user_obj>",
112 "user": "<user_obj>",
113 "review_status": "<review_status>",
113 "review_status": "<review_status>",
114 }
114 }
115 ...
115 ...
116 ]
116 ]
117 },
117 },
118 "error": null
118 "error": null
119 """
119 """
120 get_repo_or_error(repoid)
120 get_repo_or_error(repoid)
121 pull_request = get_pull_request_or_error(pullrequestid)
121 pull_request = get_pull_request_or_error(pullrequestid)
122 if not PullRequestModel().check_user_read(
122 if not PullRequestModel().check_user_read(
123 pull_request, apiuser, api=True):
123 pull_request, apiuser, api=True):
124 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
124 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
125 data = pull_request.get_api_data()
125 data = pull_request.get_api_data()
126 return data
126 return data
127
127
128
128
129 @jsonrpc_method()
129 @jsonrpc_method()
130 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
130 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
131 """
131 """
132 Get all pull requests from the repository specified in `repoid`.
132 Get all pull requests from the repository specified in `repoid`.
133
133
134 :param apiuser: This is filled automatically from the |authtoken|.
134 :param apiuser: This is filled automatically from the |authtoken|.
135 :type apiuser: AuthUser
135 :type apiuser: AuthUser
136 :param repoid: Repository name or repository ID.
136 :param repoid: Repository name or repository ID.
137 :type repoid: str or int
137 :type repoid: str or int
138 :param status: Only return pull requests with the specified status.
138 :param status: Only return pull requests with the specified status.
139 Valid options are.
139 Valid options are.
140 * ``new`` (default)
140 * ``new`` (default)
141 * ``open``
141 * ``open``
142 * ``closed``
142 * ``closed``
143 :type status: str
143 :type status: str
144
144
145 Example output:
145 Example output:
146
146
147 .. code-block:: bash
147 .. code-block:: bash
148
148
149 "id": <id_given_in_input>,
149 "id": <id_given_in_input>,
150 "result":
150 "result":
151 [
151 [
152 ...
152 ...
153 {
153 {
154 "pull_request_id": "<pull_request_id>",
154 "pull_request_id": "<pull_request_id>",
155 "url": "<url>",
155 "url": "<url>",
156 "title" : "<title>",
156 "title" : "<title>",
157 "description": "<description>",
157 "description": "<description>",
158 "status": "<status>",
158 "status": "<status>",
159 "created_on": "<date_time_created>",
159 "created_on": "<date_time_created>",
160 "updated_on": "<date_time_updated>",
160 "updated_on": "<date_time_updated>",
161 "commit_ids": [
161 "commit_ids": [
162 ...
162 ...
163 "<commit_id>",
163 "<commit_id>",
164 "<commit_id>",
164 "<commit_id>",
165 ...
165 ...
166 ],
166 ],
167 "review_status": "<review_status>",
167 "review_status": "<review_status>",
168 "mergeable": {
168 "mergeable": {
169 "status": "<bool>",
169 "status": "<bool>",
170 "message: "<message>",
170 "message: "<message>",
171 },
171 },
172 "source": {
172 "source": {
173 "clone_url": "<clone_url>",
173 "clone_url": "<clone_url>",
174 "reference":
174 "reference":
175 {
175 {
176 "name": "<name>",
176 "name": "<name>",
177 "type": "<type>",
177 "type": "<type>",
178 "commit_id": "<commit_id>",
178 "commit_id": "<commit_id>",
179 }
179 }
180 },
180 },
181 "target": {
181 "target": {
182 "clone_url": "<clone_url>",
182 "clone_url": "<clone_url>",
183 "reference":
183 "reference":
184 {
184 {
185 "name": "<name>",
185 "name": "<name>",
186 "type": "<type>",
186 "type": "<type>",
187 "commit_id": "<commit_id>",
187 "commit_id": "<commit_id>",
188 }
188 }
189 },
189 },
190 "merge": {
190 "merge": {
191 "clone_url": "<clone_url>",
191 "clone_url": "<clone_url>",
192 "reference":
192 "reference":
193 {
193 {
194 "name": "<name>",
194 "name": "<name>",
195 "type": "<type>",
195 "type": "<type>",
196 "commit_id": "<commit_id>",
196 "commit_id": "<commit_id>",
197 }
197 }
198 },
198 },
199 "author": <user_obj>,
199 "author": <user_obj>,
200 "reviewers": [
200 "reviewers": [
201 ...
201 ...
202 {
202 {
203 "user": "<user_obj>",
203 "user": "<user_obj>",
204 "review_status": "<review_status>",
204 "review_status": "<review_status>",
205 }
205 }
206 ...
206 ...
207 ]
207 ]
208 }
208 }
209 ...
209 ...
210 ],
210 ],
211 "error": null
211 "error": null
212
212
213 """
213 """
214 repo = get_repo_or_error(repoid)
214 repo = get_repo_or_error(repoid)
215 if not has_superadmin_permission(apiuser):
215 if not has_superadmin_permission(apiuser):
216 _perms = (
216 _perms = (
217 'repository.admin', 'repository.write', 'repository.read',)
217 'repository.admin', 'repository.write', 'repository.read',)
218 validate_repo_permissions(apiuser, repoid, repo, _perms)
218 validate_repo_permissions(apiuser, repoid, repo, _perms)
219
219
220 status = Optional.extract(status)
220 status = Optional.extract(status)
221 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
221 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
222 data = [pr.get_api_data() for pr in pull_requests]
222 data = [pr.get_api_data() for pr in pull_requests]
223 return data
223 return data
224
224
225
225
226 @jsonrpc_method()
226 @jsonrpc_method()
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
228 userid=Optional(OAttr('apiuser'))):
228 userid=Optional(OAttr('apiuser'))):
229 """
229 """
230 Merge the pull request specified by `pullrequestid` into its target
230 Merge the pull request specified by `pullrequestid` into its target
231 repository.
231 repository.
232
232
233 :param apiuser: This is filled automatically from the |authtoken|.
233 :param apiuser: This is filled automatically from the |authtoken|.
234 :type apiuser: AuthUser
234 :type apiuser: AuthUser
235 :param repoid: The Repository name or repository ID of the
235 :param repoid: The Repository name or repository ID of the
236 target repository to which the |pr| is to be merged.
236 target repository to which the |pr| is to be merged.
237 :type repoid: str or int
237 :type repoid: str or int
238 :param pullrequestid: ID of the pull request which shall be merged.
238 :param pullrequestid: ID of the pull request which shall be merged.
239 :type pullrequestid: int
239 :type pullrequestid: int
240 :param userid: Merge the pull request as this user.
240 :param userid: Merge the pull request as this user.
241 :type userid: Optional(str or int)
241 :type userid: Optional(str or int)
242
242
243 Example output:
243 Example output:
244
244
245 .. code-block:: bash
245 .. code-block:: bash
246
246
247 "id": <id_given_in_input>,
247 "id": <id_given_in_input>,
248 "result":
248 "result":
249 {
249 {
250 "executed": "<bool>",
250 "executed": "<bool>",
251 "failure_reason": "<int>",
251 "failure_reason": "<int>",
252 "merge_commit_id": "<merge_commit_id>",
252 "merge_commit_id": "<merge_commit_id>",
253 "possible": "<bool>",
253 "possible": "<bool>",
254 "merge_ref": {
254 "merge_ref": {
255 "commit_id": "<commit_id>",
255 "commit_id": "<commit_id>",
256 "type": "<type>",
256 "type": "<type>",
257 "name": "<name>"
257 "name": "<name>"
258 }
258 }
259 },
259 },
260 "error": null
260 "error": null
261
261
262 """
262 """
263 repo = get_repo_or_error(repoid)
263 repo = get_repo_or_error(repoid)
264 if not isinstance(userid, Optional):
264 if not isinstance(userid, Optional):
265 if (has_superadmin_permission(apiuser) or
265 if (has_superadmin_permission(apiuser) or
266 HasRepoPermissionAnyApi('repository.admin')(
266 HasRepoPermissionAnyApi('repository.admin')(
267 user=apiuser, repo_name=repo.repo_name)):
267 user=apiuser, repo_name=repo.repo_name)):
268 apiuser = get_user_or_error(userid)
268 apiuser = get_user_or_error(userid)
269 else:
269 else:
270 raise JSONRPCError('userid is not the same as your user')
270 raise JSONRPCError('userid is not the same as your user')
271
271
272 pull_request = get_pull_request_or_error(pullrequestid)
272 pull_request = get_pull_request_or_error(pullrequestid)
273 if not PullRequestModel().check_user_merge(
273
274 pull_request, apiuser, api=True):
274 check = MergeCheck.validate(pull_request, user=apiuser)
275 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
275 merge_possible = not check.failed
276 if pull_request.is_closed():
276
277 if not merge_possible:
278 reasons = ','.join([msg for _e, msg in check.errors])
277 raise JSONRPCError(
279 raise JSONRPCError(
278 'pull request `%s` merge failed, pull request is closed' % (
280 'merge not possible for following reasons: {}'.format(reasons))
279 pullrequestid,))
280
281
281 target_repo = pull_request.target_repo
282 target_repo = pull_request.target_repo
282 extras = vcs_operation_context(
283 extras = vcs_operation_context(
283 request.environ, repo_name=target_repo.repo_name,
284 request.environ, repo_name=target_repo.repo_name,
284 username=apiuser.username, action='push',
285 username=apiuser.username, action='push',
285 scm=target_repo.repo_type)
286 scm=target_repo.repo_type)
286 merge_response = PullRequestModel().merge(
287 merge_response = PullRequestModel().merge(
287 pull_request, apiuser, extras=extras)
288 pull_request, apiuser, extras=extras)
288 if merge_response.executed:
289 if merge_response.executed:
289 PullRequestModel().close_pull_request(
290 PullRequestModel().close_pull_request(
290 pull_request.pull_request_id, apiuser)
291 pull_request.pull_request_id, apiuser)
291
292
292 Session().commit()
293 Session().commit()
293
294
294 # In previous versions the merge response directly contained the merge
295 # In previous versions the merge response directly contained the merge
295 # commit id. It is now contained in the merge reference object. To be
296 # commit id. It is now contained in the merge reference object. To be
296 # backwards compatible we have to extract it again.
297 # backwards compatible we have to extract it again.
297 merge_response = merge_response._asdict()
298 merge_response = merge_response._asdict()
298 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
299 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
299
300
300 return merge_response
301 return merge_response
301
302
302
303
303 @jsonrpc_method()
304 @jsonrpc_method()
304 def close_pull_request(request, apiuser, repoid, pullrequestid,
305 def close_pull_request(request, apiuser, repoid, pullrequestid,
305 userid=Optional(OAttr('apiuser'))):
306 userid=Optional(OAttr('apiuser'))):
306 """
307 """
307 Close the pull request specified by `pullrequestid`.
308 Close the pull request specified by `pullrequestid`.
308
309
309 :param apiuser: This is filled automatically from the |authtoken|.
310 :param apiuser: This is filled automatically from the |authtoken|.
310 :type apiuser: AuthUser
311 :type apiuser: AuthUser
311 :param repoid: Repository name or repository ID to which the pull
312 :param repoid: Repository name or repository ID to which the pull
312 request belongs.
313 request belongs.
313 :type repoid: str or int
314 :type repoid: str or int
314 :param pullrequestid: ID of the pull request to be closed.
315 :param pullrequestid: ID of the pull request to be closed.
315 :type pullrequestid: int
316 :type pullrequestid: int
316 :param userid: Close the pull request as this user.
317 :param userid: Close the pull request as this user.
317 :type userid: Optional(str or int)
318 :type userid: Optional(str or int)
318
319
319 Example output:
320 Example output:
320
321
321 .. code-block:: bash
322 .. code-block:: bash
322
323
323 "id": <id_given_in_input>,
324 "id": <id_given_in_input>,
324 "result":
325 "result":
325 {
326 {
326 "pull_request_id": "<int>",
327 "pull_request_id": "<int>",
327 "closed": "<bool>"
328 "closed": "<bool>"
328 },
329 },
329 "error": null
330 "error": null
330
331
331 """
332 """
332 repo = get_repo_or_error(repoid)
333 repo = get_repo_or_error(repoid)
333 if not isinstance(userid, Optional):
334 if not isinstance(userid, Optional):
334 if (has_superadmin_permission(apiuser) or
335 if (has_superadmin_permission(apiuser) or
335 HasRepoPermissionAnyApi('repository.admin')(
336 HasRepoPermissionAnyApi('repository.admin')(
336 user=apiuser, repo_name=repo.repo_name)):
337 user=apiuser, repo_name=repo.repo_name)):
337 apiuser = get_user_or_error(userid)
338 apiuser = get_user_or_error(userid)
338 else:
339 else:
339 raise JSONRPCError('userid is not the same as your user')
340 raise JSONRPCError('userid is not the same as your user')
340
341
341 pull_request = get_pull_request_or_error(pullrequestid)
342 pull_request = get_pull_request_or_error(pullrequestid)
342 if not PullRequestModel().check_user_update(
343 if not PullRequestModel().check_user_update(
343 pull_request, apiuser, api=True):
344 pull_request, apiuser, api=True):
344 raise JSONRPCError(
345 raise JSONRPCError(
345 'pull request `%s` close failed, no permission to close.' % (
346 'pull request `%s` close failed, no permission to close.' % (
346 pullrequestid,))
347 pullrequestid,))
347 if pull_request.is_closed():
348 if pull_request.is_closed():
348 raise JSONRPCError(
349 raise JSONRPCError(
349 'pull request `%s` is already closed' % (pullrequestid,))
350 'pull request `%s` is already closed' % (pullrequestid,))
350
351
351 PullRequestModel().close_pull_request(
352 PullRequestModel().close_pull_request(
352 pull_request.pull_request_id, apiuser)
353 pull_request.pull_request_id, apiuser)
353 Session().commit()
354 Session().commit()
354 data = {
355 data = {
355 'pull_request_id': pull_request.pull_request_id,
356 'pull_request_id': pull_request.pull_request_id,
356 'closed': True,
357 'closed': True,
357 }
358 }
358 return data
359 return data
359
360
360
361
361 @jsonrpc_method()
362 @jsonrpc_method()
362 def comment_pull_request(request, apiuser, repoid, pullrequestid,
363 def comment_pull_request(request, apiuser, repoid, pullrequestid,
363 message=Optional(None), status=Optional(None),
364 message=Optional(None), status=Optional(None),
364 commit_id=Optional(None),
365 commit_id=Optional(None),
365 userid=Optional(OAttr('apiuser'))):
366 userid=Optional(OAttr('apiuser'))):
366 """
367 """
367 Comment on the pull request specified with the `pullrequestid`,
368 Comment on the pull request specified with the `pullrequestid`,
368 in the |repo| specified by the `repoid`, and optionally change the
369 in the |repo| specified by the `repoid`, and optionally change the
369 review status.
370 review status.
370
371
371 :param apiuser: This is filled automatically from the |authtoken|.
372 :param apiuser: This is filled automatically from the |authtoken|.
372 :type apiuser: AuthUser
373 :type apiuser: AuthUser
373 :param repoid: The repository name or repository ID.
374 :param repoid: The repository name or repository ID.
374 :type repoid: str or int
375 :type repoid: str or int
375 :param pullrequestid: The pull request ID.
376 :param pullrequestid: The pull request ID.
376 :type pullrequestid: int
377 :type pullrequestid: int
377 :param message: The text content of the comment.
378 :param message: The text content of the comment.
378 :type message: str
379 :type message: str
379 :param status: (**Optional**) Set the approval status of the pull
380 :param status: (**Optional**) Set the approval status of the pull
380 request. Valid options are:
381 request. Valid options are:
381 * not_reviewed
382 * not_reviewed
382 * approved
383 * approved
383 * rejected
384 * rejected
384 * under_review
385 * under_review
385 :type status: str
386 :type status: str
386 :param commit_id: Specify the commit_id for which to set a comment. If
387 :param commit_id: Specify the commit_id for which to set a comment. If
387 given commit_id is different than latest in the PR status
388 given commit_id is different than latest in the PR status
388 change won't be performed.
389 change won't be performed.
389 :type commit_id: str
390 :type commit_id: str
390 :param userid: Comment on the pull request as this user
391 :param userid: Comment on the pull request as this user
391 :type userid: Optional(str or int)
392 :type userid: Optional(str or int)
392
393
393 Example output:
394 Example output:
394
395
395 .. code-block:: bash
396 .. code-block:: bash
396
397
397 id : <id_given_in_input>
398 id : <id_given_in_input>
398 result :
399 result :
399 {
400 {
400 "pull_request_id": "<Integer>",
401 "pull_request_id": "<Integer>",
401 "comment_id": "<Integer>",
402 "comment_id": "<Integer>",
402 "status": {"given": <given_status>,
403 "status": {"given": <given_status>,
403 "was_changed": <bool status_was_actually_changed> },
404 "was_changed": <bool status_was_actually_changed> },
404 }
405 }
405 error : null
406 error : null
406 """
407 """
407 repo = get_repo_or_error(repoid)
408 repo = get_repo_or_error(repoid)
408 if not isinstance(userid, Optional):
409 if not isinstance(userid, Optional):
409 if (has_superadmin_permission(apiuser) or
410 if (has_superadmin_permission(apiuser) or
410 HasRepoPermissionAnyApi('repository.admin')(
411 HasRepoPermissionAnyApi('repository.admin')(
411 user=apiuser, repo_name=repo.repo_name)):
412 user=apiuser, repo_name=repo.repo_name)):
412 apiuser = get_user_or_error(userid)
413 apiuser = get_user_or_error(userid)
413 else:
414 else:
414 raise JSONRPCError('userid is not the same as your user')
415 raise JSONRPCError('userid is not the same as your user')
415
416
416 pull_request = get_pull_request_or_error(pullrequestid)
417 pull_request = get_pull_request_or_error(pullrequestid)
417 if not PullRequestModel().check_user_read(
418 if not PullRequestModel().check_user_read(
418 pull_request, apiuser, api=True):
419 pull_request, apiuser, api=True):
419 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
420 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
420 message = Optional.extract(message)
421 message = Optional.extract(message)
421 status = Optional.extract(status)
422 status = Optional.extract(status)
422 commit_id = Optional.extract(commit_id)
423 commit_id = Optional.extract(commit_id)
423
424
424 if not message and not status:
425 if not message and not status:
425 raise JSONRPCError(
426 raise JSONRPCError(
426 'Both message and status parameters are missing. '
427 'Both message and status parameters are missing. '
427 'At least one is required.')
428 'At least one is required.')
428
429
429 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
430 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
430 status is not None):
431 status is not None):
431 raise JSONRPCError('Unknown comment status: `%s`' % status)
432 raise JSONRPCError('Unknown comment status: `%s`' % status)
432
433
433 if commit_id and commit_id not in pull_request.revisions:
434 if commit_id and commit_id not in pull_request.revisions:
434 raise JSONRPCError(
435 raise JSONRPCError(
435 'Invalid commit_id `%s` for this pull request.' % commit_id)
436 'Invalid commit_id `%s` for this pull request.' % commit_id)
436
437
437 allowed_to_change_status = PullRequestModel().check_user_change_status(
438 allowed_to_change_status = PullRequestModel().check_user_change_status(
438 pull_request, apiuser)
439 pull_request, apiuser)
439
440
440 # if commit_id is passed re-validated if user is allowed to change status
441 # if commit_id is passed re-validated if user is allowed to change status
441 # based on latest commit_id from the PR
442 # based on latest commit_id from the PR
442 if commit_id:
443 if commit_id:
443 commit_idx = pull_request.revisions.index(commit_id)
444 commit_idx = pull_request.revisions.index(commit_id)
444 if commit_idx != 0:
445 if commit_idx != 0:
445 allowed_to_change_status = False
446 allowed_to_change_status = False
446
447
447 text = message
448 text = message
448 status_label = ChangesetStatus.get_status_lbl(status)
449 status_label = ChangesetStatus.get_status_lbl(status)
449 if status and allowed_to_change_status:
450 if status and allowed_to_change_status:
450 st_message = ('Status change %(transition_icon)s %(status)s'
451 st_message = ('Status change %(transition_icon)s %(status)s'
451 % {'transition_icon': '>', 'status': status_label})
452 % {'transition_icon': '>', 'status': status_label})
452 text = message or st_message
453 text = message or st_message
453
454
454 rc_config = SettingsModel().get_all_settings()
455 rc_config = SettingsModel().get_all_settings()
455 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
456 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
456
457
457 status_change = status and allowed_to_change_status
458 status_change = status and allowed_to_change_status
458 comment = CommentsModel().create(
459 comment = CommentsModel().create(
459 text=text,
460 text=text,
460 repo=pull_request.target_repo.repo_id,
461 repo=pull_request.target_repo.repo_id,
461 user=apiuser.user_id,
462 user=apiuser.user_id,
462 pull_request=pull_request.pull_request_id,
463 pull_request=pull_request.pull_request_id,
463 f_path=None,
464 f_path=None,
464 line_no=None,
465 line_no=None,
465 status_change=(status_label if status_change else None),
466 status_change=(status_label if status_change else None),
466 status_change_type=(status if status_change else None),
467 status_change_type=(status if status_change else None),
467 closing_pr=False,
468 closing_pr=False,
468 renderer=renderer
469 renderer=renderer
469 )
470 )
470
471
471 if allowed_to_change_status and status:
472 if allowed_to_change_status and status:
472 ChangesetStatusModel().set_status(
473 ChangesetStatusModel().set_status(
473 pull_request.target_repo.repo_id,
474 pull_request.target_repo.repo_id,
474 status,
475 status,
475 apiuser.user_id,
476 apiuser.user_id,
476 comment,
477 comment,
477 pull_request=pull_request.pull_request_id
478 pull_request=pull_request.pull_request_id
478 )
479 )
479 Session().flush()
480 Session().flush()
480
481
481 Session().commit()
482 Session().commit()
482 data = {
483 data = {
483 'pull_request_id': pull_request.pull_request_id,
484 'pull_request_id': pull_request.pull_request_id,
484 'comment_id': comment.comment_id if comment else None,
485 'comment_id': comment.comment_id if comment else None,
485 'status': {'given': status, 'was_changed': status_change},
486 'status': {'given': status, 'was_changed': status_change},
486 }
487 }
487 return data
488 return data
488
489
489
490
490 @jsonrpc_method()
491 @jsonrpc_method()
491 def create_pull_request(
492 def create_pull_request(
492 request, apiuser, source_repo, target_repo, source_ref, target_ref,
493 request, apiuser, source_repo, target_repo, source_ref, target_ref,
493 title, description=Optional(''), reviewers=Optional(None)):
494 title, description=Optional(''), reviewers=Optional(None)):
494 """
495 """
495 Creates a new pull request.
496 Creates a new pull request.
496
497
497 Accepts refs in the following formats:
498 Accepts refs in the following formats:
498
499
499 * branch:<branch_name>:<sha>
500 * branch:<branch_name>:<sha>
500 * branch:<branch_name>
501 * branch:<branch_name>
501 * bookmark:<bookmark_name>:<sha> (Mercurial only)
502 * bookmark:<bookmark_name>:<sha> (Mercurial only)
502 * bookmark:<bookmark_name> (Mercurial only)
503 * bookmark:<bookmark_name> (Mercurial only)
503
504
504 :param apiuser: This is filled automatically from the |authtoken|.
505 :param apiuser: This is filled automatically from the |authtoken|.
505 :type apiuser: AuthUser
506 :type apiuser: AuthUser
506 :param source_repo: Set the source repository name.
507 :param source_repo: Set the source repository name.
507 :type source_repo: str
508 :type source_repo: str
508 :param target_repo: Set the target repository name.
509 :param target_repo: Set the target repository name.
509 :type target_repo: str
510 :type target_repo: str
510 :param source_ref: Set the source ref name.
511 :param source_ref: Set the source ref name.
511 :type source_ref: str
512 :type source_ref: str
512 :param target_ref: Set the target ref name.
513 :param target_ref: Set the target ref name.
513 :type target_ref: str
514 :type target_ref: str
514 :param title: Set the pull request title.
515 :param title: Set the pull request title.
515 :type title: str
516 :type title: str
516 :param description: Set the pull request description.
517 :param description: Set the pull request description.
517 :type description: Optional(str)
518 :type description: Optional(str)
518 :param reviewers: Set the new pull request reviewers list.
519 :param reviewers: Set the new pull request reviewers list.
519 :type reviewers: Optional(list)
520 :type reviewers: Optional(list)
520 Accepts username strings or objects of the format:
521 Accepts username strings or objects of the format:
521 {
522 {
522 'username': 'nick', 'reasons': ['original author']
523 'username': 'nick', 'reasons': ['original author']
523 }
524 }
524 """
525 """
525
526
526 source = get_repo_or_error(source_repo)
527 source = get_repo_or_error(source_repo)
527 target = get_repo_or_error(target_repo)
528 target = get_repo_or_error(target_repo)
528 if not has_superadmin_permission(apiuser):
529 if not has_superadmin_permission(apiuser):
529 _perms = ('repository.admin', 'repository.write', 'repository.read',)
530 _perms = ('repository.admin', 'repository.write', 'repository.read',)
530 validate_repo_permissions(apiuser, source_repo, source, _perms)
531 validate_repo_permissions(apiuser, source_repo, source, _perms)
531
532
532 full_source_ref = resolve_ref_or_error(source_ref, source)
533 full_source_ref = resolve_ref_or_error(source_ref, source)
533 full_target_ref = resolve_ref_or_error(target_ref, target)
534 full_target_ref = resolve_ref_or_error(target_ref, target)
534 source_commit = get_commit_or_error(full_source_ref, source)
535 source_commit = get_commit_or_error(full_source_ref, source)
535 target_commit = get_commit_or_error(full_target_ref, target)
536 target_commit = get_commit_or_error(full_target_ref, target)
536 source_scm = source.scm_instance()
537 source_scm = source.scm_instance()
537 target_scm = target.scm_instance()
538 target_scm = target.scm_instance()
538
539
539 commit_ranges = target_scm.compare(
540 commit_ranges = target_scm.compare(
540 target_commit.raw_id, source_commit.raw_id, source_scm,
541 target_commit.raw_id, source_commit.raw_id, source_scm,
541 merge=True, pre_load=[])
542 merge=True, pre_load=[])
542
543
543 ancestor = target_scm.get_common_ancestor(
544 ancestor = target_scm.get_common_ancestor(
544 target_commit.raw_id, source_commit.raw_id, source_scm)
545 target_commit.raw_id, source_commit.raw_id, source_scm)
545
546
546 if not commit_ranges:
547 if not commit_ranges:
547 raise JSONRPCError('no commits found')
548 raise JSONRPCError('no commits found')
548
549
549 if not ancestor:
550 if not ancestor:
550 raise JSONRPCError('no common ancestor found')
551 raise JSONRPCError('no common ancestor found')
551
552
552 reviewer_objects = Optional.extract(reviewers) or []
553 reviewer_objects = Optional.extract(reviewers) or []
553 if not isinstance(reviewer_objects, list):
554 if not isinstance(reviewer_objects, list):
554 raise JSONRPCError('reviewers should be specified as a list')
555 raise JSONRPCError('reviewers should be specified as a list')
555
556
556 reviewers_reasons = []
557 reviewers_reasons = []
557 for reviewer_object in reviewer_objects:
558 for reviewer_object in reviewer_objects:
558 reviewer_reasons = []
559 reviewer_reasons = []
559 if isinstance(reviewer_object, (basestring, int)):
560 if isinstance(reviewer_object, (basestring, int)):
560 reviewer_username = reviewer_object
561 reviewer_username = reviewer_object
561 else:
562 else:
562 reviewer_username = reviewer_object['username']
563 reviewer_username = reviewer_object['username']
563 reviewer_reasons = reviewer_object.get('reasons', [])
564 reviewer_reasons = reviewer_object.get('reasons', [])
564
565
565 user = get_user_or_error(reviewer_username)
566 user = get_user_or_error(reviewer_username)
566 reviewers_reasons.append((user.user_id, reviewer_reasons))
567 reviewers_reasons.append((user.user_id, reviewer_reasons))
567
568
568 pull_request_model = PullRequestModel()
569 pull_request_model = PullRequestModel()
569 pull_request = pull_request_model.create(
570 pull_request = pull_request_model.create(
570 created_by=apiuser.user_id,
571 created_by=apiuser.user_id,
571 source_repo=source_repo,
572 source_repo=source_repo,
572 source_ref=full_source_ref,
573 source_ref=full_source_ref,
573 target_repo=target_repo,
574 target_repo=target_repo,
574 target_ref=full_target_ref,
575 target_ref=full_target_ref,
575 revisions=reversed(
576 revisions=reversed(
576 [commit.raw_id for commit in reversed(commit_ranges)]),
577 [commit.raw_id for commit in reversed(commit_ranges)]),
577 reviewers=reviewers_reasons,
578 reviewers=reviewers_reasons,
578 title=title,
579 title=title,
579 description=Optional.extract(description)
580 description=Optional.extract(description)
580 )
581 )
581
582
582 Session().commit()
583 Session().commit()
583 data = {
584 data = {
584 'msg': 'Created new pull request `{}`'.format(title),
585 'msg': 'Created new pull request `{}`'.format(title),
585 'pull_request_id': pull_request.pull_request_id,
586 'pull_request_id': pull_request.pull_request_id,
586 }
587 }
587 return data
588 return data
588
589
589
590
590 @jsonrpc_method()
591 @jsonrpc_method()
591 def update_pull_request(
592 def update_pull_request(
592 request, apiuser, repoid, pullrequestid, title=Optional(''),
593 request, apiuser, repoid, pullrequestid, title=Optional(''),
593 description=Optional(''), reviewers=Optional(None),
594 description=Optional(''), reviewers=Optional(None),
594 update_commits=Optional(None), close_pull_request=Optional(None)):
595 update_commits=Optional(None), close_pull_request=Optional(None)):
595 """
596 """
596 Updates a pull request.
597 Updates a pull request.
597
598
598 :param apiuser: This is filled automatically from the |authtoken|.
599 :param apiuser: This is filled automatically from the |authtoken|.
599 :type apiuser: AuthUser
600 :type apiuser: AuthUser
600 :param repoid: The repository name or repository ID.
601 :param repoid: The repository name or repository ID.
601 :type repoid: str or int
602 :type repoid: str or int
602 :param pullrequestid: The pull request ID.
603 :param pullrequestid: The pull request ID.
603 :type pullrequestid: int
604 :type pullrequestid: int
604 :param title: Set the pull request title.
605 :param title: Set the pull request title.
605 :type title: str
606 :type title: str
606 :param description: Update pull request description.
607 :param description: Update pull request description.
607 :type description: Optional(str)
608 :type description: Optional(str)
608 :param reviewers: Update pull request reviewers list with new value.
609 :param reviewers: Update pull request reviewers list with new value.
609 :type reviewers: Optional(list)
610 :type reviewers: Optional(list)
610 :param update_commits: Trigger update of commits for this pull request
611 :param update_commits: Trigger update of commits for this pull request
611 :type: update_commits: Optional(bool)
612 :type: update_commits: Optional(bool)
612 :param close_pull_request: Close this pull request with rejected state
613 :param close_pull_request: Close this pull request with rejected state
613 :type: close_pull_request: Optional(bool)
614 :type: close_pull_request: Optional(bool)
614
615
615 Example output:
616 Example output:
616
617
617 .. code-block:: bash
618 .. code-block:: bash
618
619
619 id : <id_given_in_input>
620 id : <id_given_in_input>
620 result :
621 result :
621 {
622 {
622 "msg": "Updated pull request `63`",
623 "msg": "Updated pull request `63`",
623 "pull_request": <pull_request_object>,
624 "pull_request": <pull_request_object>,
624 "updated_reviewers": {
625 "updated_reviewers": {
625 "added": [
626 "added": [
626 "username"
627 "username"
627 ],
628 ],
628 "removed": []
629 "removed": []
629 },
630 },
630 "updated_commits": {
631 "updated_commits": {
631 "added": [
632 "added": [
632 "<sha1_hash>"
633 "<sha1_hash>"
633 ],
634 ],
634 "common": [
635 "common": [
635 "<sha1_hash>",
636 "<sha1_hash>",
636 "<sha1_hash>",
637 "<sha1_hash>",
637 ],
638 ],
638 "removed": []
639 "removed": []
639 }
640 }
640 }
641 }
641 error : null
642 error : null
642 """
643 """
643
644
644 repo = get_repo_or_error(repoid)
645 repo = get_repo_or_error(repoid)
645 pull_request = get_pull_request_or_error(pullrequestid)
646 pull_request = get_pull_request_or_error(pullrequestid)
646 if not PullRequestModel().check_user_update(
647 if not PullRequestModel().check_user_update(
647 pull_request, apiuser, api=True):
648 pull_request, apiuser, api=True):
648 raise JSONRPCError(
649 raise JSONRPCError(
649 'pull request `%s` update failed, no permission to update.' % (
650 'pull request `%s` update failed, no permission to update.' % (
650 pullrequestid,))
651 pullrequestid,))
651 if pull_request.is_closed():
652 if pull_request.is_closed():
652 raise JSONRPCError(
653 raise JSONRPCError(
653 'pull request `%s` update failed, pull request is closed' % (
654 'pull request `%s` update failed, pull request is closed' % (
654 pullrequestid,))
655 pullrequestid,))
655
656
656 reviewer_objects = Optional.extract(reviewers) or []
657 reviewer_objects = Optional.extract(reviewers) or []
657 if not isinstance(reviewer_objects, list):
658 if not isinstance(reviewer_objects, list):
658 raise JSONRPCError('reviewers should be specified as a list')
659 raise JSONRPCError('reviewers should be specified as a list')
659
660
660 reviewers_reasons = []
661 reviewers_reasons = []
661 reviewer_ids = set()
662 reviewer_ids = set()
662 for reviewer_object in reviewer_objects:
663 for reviewer_object in reviewer_objects:
663 reviewer_reasons = []
664 reviewer_reasons = []
664 if isinstance(reviewer_object, (int, basestring)):
665 if isinstance(reviewer_object, (int, basestring)):
665 reviewer_username = reviewer_object
666 reviewer_username = reviewer_object
666 else:
667 else:
667 reviewer_username = reviewer_object['username']
668 reviewer_username = reviewer_object['username']
668 reviewer_reasons = reviewer_object.get('reasons', [])
669 reviewer_reasons = reviewer_object.get('reasons', [])
669
670
670 user = get_user_or_error(reviewer_username)
671 user = get_user_or_error(reviewer_username)
671 reviewer_ids.add(user.user_id)
672 reviewer_ids.add(user.user_id)
672 reviewers_reasons.append((user.user_id, reviewer_reasons))
673 reviewers_reasons.append((user.user_id, reviewer_reasons))
673
674
674 title = Optional.extract(title)
675 title = Optional.extract(title)
675 description = Optional.extract(description)
676 description = Optional.extract(description)
676 if title or description:
677 if title or description:
677 PullRequestModel().edit(
678 PullRequestModel().edit(
678 pull_request, title or pull_request.title,
679 pull_request, title or pull_request.title,
679 description or pull_request.description)
680 description or pull_request.description)
680 Session().commit()
681 Session().commit()
681
682
682 commit_changes = {"added": [], "common": [], "removed": []}
683 commit_changes = {"added": [], "common": [], "removed": []}
683 if str2bool(Optional.extract(update_commits)):
684 if str2bool(Optional.extract(update_commits)):
684 if PullRequestModel().has_valid_update_type(pull_request):
685 if PullRequestModel().has_valid_update_type(pull_request):
685 update_response = PullRequestModel().update_commits(
686 update_response = PullRequestModel().update_commits(
686 pull_request)
687 pull_request)
687 commit_changes = update_response.changes or commit_changes
688 commit_changes = update_response.changes or commit_changes
688 Session().commit()
689 Session().commit()
689
690
690 reviewers_changes = {"added": [], "removed": []}
691 reviewers_changes = {"added": [], "removed": []}
691 if reviewer_ids:
692 if reviewer_ids:
692 added_reviewers, removed_reviewers = \
693 added_reviewers, removed_reviewers = \
693 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
694 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
694
695
695 reviewers_changes['added'] = sorted(
696 reviewers_changes['added'] = sorted(
696 [get_user_or_error(n).username for n in added_reviewers])
697 [get_user_or_error(n).username for n in added_reviewers])
697 reviewers_changes['removed'] = sorted(
698 reviewers_changes['removed'] = sorted(
698 [get_user_or_error(n).username for n in removed_reviewers])
699 [get_user_or_error(n).username for n in removed_reviewers])
699 Session().commit()
700 Session().commit()
700
701
701 if str2bool(Optional.extract(close_pull_request)):
702 if str2bool(Optional.extract(close_pull_request)):
702 PullRequestModel().close_pull_request_with_comment(
703 PullRequestModel().close_pull_request_with_comment(
703 pull_request, apiuser, repo)
704 pull_request, apiuser, repo)
704 Session().commit()
705 Session().commit()
705
706
706 data = {
707 data = {
707 'msg': 'Updated pull request `{}`'.format(
708 'msg': 'Updated pull request `{}`'.format(
708 pull_request.pull_request_id),
709 pull_request.pull_request_id),
709 'pull_request': pull_request.get_api_data(),
710 'pull_request': pull_request.get_api_data(),
710 'updated_commits': commit_changes,
711 'updated_commits': commit_changes,
711 'updated_reviewers': reviewers_changes
712 'updated_reviewers': reviewers_changes
712 }
713 }
713
714
714 return data
715 return data
@@ -1,1046 +1,1009 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 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.mako')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.mako')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = CommentsModel().get_all_comments(
261 comments = CommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
348 c.default_source_ref = selected_source_ref
349
349
350 return render('/pullrequests/pullrequest.mako')
350 return render('/pullrequests/pullrequest.mako')
351
351
352 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After succesfull merging, the pull request is automatically
600 After successful merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 if self._meets_merge_pre_conditions(pull_request, user):
607 check = MergeCheck.validate(pull_request, user)
608 merge_possible = not check.failed
609
610 for err_type, error_msg in check.errors:
611 h.flash(error_msg, category=err_type)
612
613 if merge_possible:
608 log.debug("Pre-conditions checked, trying to merge.")
614 log.debug("Pre-conditions checked, trying to merge.")
609 extras = vcs_operation_context(
615 extras = vcs_operation_context(
610 request.environ, repo_name=pull_request.target_repo.repo_name,
616 request.environ, repo_name=pull_request.target_repo.repo_name,
611 username=user.username, action='push',
617 username=user.username, action='push',
612 scm=pull_request.target_repo.repo_type)
618 scm=pull_request.target_repo.repo_type)
613 self._merge_pull_request(pull_request, user, extras)
619 self._merge_pull_request(pull_request, user, extras)
614
620
615 return redirect(url(
621 return redirect(url(
616 'pullrequest_show',
622 'pullrequest_show',
617 repo_name=pull_request.target_repo.repo_name,
623 repo_name=pull_request.target_repo.repo_name,
618 pull_request_id=pull_request.pull_request_id))
624 pull_request_id=pull_request.pull_request_id))
619
625
620 def _meets_merge_pre_conditions(self, pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
622 raise HTTPForbidden()
623
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 if not merge_status:
626 log.debug("Cannot merge, not mergeable.")
627 h.flash(msg, category='error')
628 return False
629
630 if (pull_request.calculated_review_status()
631 is not ChangesetStatus.STATUS_APPROVED):
632 log.debug("Cannot merge, approval is pending.")
633 msg = _('Pull request reviewer approval is pending.')
634 h.flash(msg, category='error')
635 return False
636
637 todos = CommentsModel().get_unresolved_todos(pull_request)
638 if todos:
639 log.debug("Cannot merge, unresolved todos left.")
640 if len(todos) == 1:
641 msg = _('Cannot merge, {} todo still not resolved.').format(
642 len(todos))
643 else:
644 msg = _('Cannot merge, {} todos still not resolved.').format(
645 len(todos))
646 h.flash(msg, category='error')
647 return False
648 return True
649
650 def _merge_pull_request(self, pull_request, user, extras):
626 def _merge_pull_request(self, pull_request, user, extras):
651 merge_resp = PullRequestModel().merge(
627 merge_resp = PullRequestModel().merge(
652 pull_request, user, extras=extras)
628 pull_request, user, extras=extras)
653
629
654 if merge_resp.executed:
630 if merge_resp.executed:
655 log.debug("The merge was successful, closing the pull request.")
631 log.debug("The merge was successful, closing the pull request.")
656 PullRequestModel().close_pull_request(
632 PullRequestModel().close_pull_request(
657 pull_request.pull_request_id, user)
633 pull_request.pull_request_id, user)
658 Session().commit()
634 Session().commit()
659 msg = _('Pull request was successfully merged and closed.')
635 msg = _('Pull request was successfully merged and closed.')
660 h.flash(msg, category='success')
636 h.flash(msg, category='success')
661 else:
637 else:
662 log.debug(
638 log.debug(
663 "The merge was not successful. Merge response: %s",
639 "The merge was not successful. Merge response: %s",
664 merge_resp)
640 merge_resp)
665 msg = PullRequestModel().merge_status_message(
641 msg = PullRequestModel().merge_status_message(
666 merge_resp.failure_reason)
642 merge_resp.failure_reason)
667 h.flash(msg, category='error')
643 h.flash(msg, category='error')
668
644
669 def _update_reviewers(self, pull_request_id, review_members):
645 def _update_reviewers(self, pull_request_id, review_members):
670 reviewers = [
646 reviewers = [
671 (int(r['user_id']), r['reasons']) for r in review_members]
647 (int(r['user_id']), r['reasons']) for r in review_members]
672 PullRequestModel().update_reviewers(pull_request_id, reviewers)
648 PullRequestModel().update_reviewers(pull_request_id, reviewers)
673 Session().commit()
649 Session().commit()
674
650
675 def _reject_close(self, pull_request):
651 def _reject_close(self, pull_request):
676 if pull_request.is_closed():
652 if pull_request.is_closed():
677 raise HTTPForbidden()
653 raise HTTPForbidden()
678
654
679 PullRequestModel().close_pull_request_with_comment(
655 PullRequestModel().close_pull_request_with_comment(
680 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
656 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
681 Session().commit()
657 Session().commit()
682
658
683 @LoginRequired()
659 @LoginRequired()
684 @NotAnonymous()
660 @NotAnonymous()
685 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
661 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
686 'repository.admin')
662 'repository.admin')
687 @auth.CSRFRequired()
663 @auth.CSRFRequired()
688 @jsonify
664 @jsonify
689 def delete(self, repo_name, pull_request_id):
665 def delete(self, repo_name, pull_request_id):
690 pull_request_id = safe_int(pull_request_id)
666 pull_request_id = safe_int(pull_request_id)
691 pull_request = PullRequest.get_or_404(pull_request_id)
667 pull_request = PullRequest.get_or_404(pull_request_id)
692 # only owner can delete it !
668 # only owner can delete it !
693 if pull_request.author.user_id == c.rhodecode_user.user_id:
669 if pull_request.author.user_id == c.rhodecode_user.user_id:
694 PullRequestModel().delete(pull_request)
670 PullRequestModel().delete(pull_request)
695 Session().commit()
671 Session().commit()
696 h.flash(_('Successfully deleted pull request'),
672 h.flash(_('Successfully deleted pull request'),
697 category='success')
673 category='success')
698 return redirect(url('my_account_pullrequests'))
674 return redirect(url('my_account_pullrequests'))
699 raise HTTPForbidden()
675 raise HTTPForbidden()
700
676
701 def _get_pr_version(self, pull_request_id, version=None):
677 def _get_pr_version(self, pull_request_id, version=None):
702 pull_request_id = safe_int(pull_request_id)
678 pull_request_id = safe_int(pull_request_id)
703 at_version = None
679 at_version = None
704
680
705 if version and version == 'latest':
681 if version and version == 'latest':
706 pull_request_ver = PullRequest.get(pull_request_id)
682 pull_request_ver = PullRequest.get(pull_request_id)
707 pull_request_obj = pull_request_ver
683 pull_request_obj = pull_request_ver
708 _org_pull_request_obj = pull_request_obj
684 _org_pull_request_obj = pull_request_obj
709 at_version = 'latest'
685 at_version = 'latest'
710 elif version:
686 elif version:
711 pull_request_ver = PullRequestVersion.get_or_404(version)
687 pull_request_ver = PullRequestVersion.get_or_404(version)
712 pull_request_obj = pull_request_ver
688 pull_request_obj = pull_request_ver
713 _org_pull_request_obj = pull_request_ver.pull_request
689 _org_pull_request_obj = pull_request_ver.pull_request
714 at_version = pull_request_ver.pull_request_version_id
690 at_version = pull_request_ver.pull_request_version_id
715 else:
691 else:
716 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
692 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
717
693
718 pull_request_display_obj = PullRequest.get_pr_display_object(
694 pull_request_display_obj = PullRequest.get_pr_display_object(
719 pull_request_obj, _org_pull_request_obj)
695 pull_request_obj, _org_pull_request_obj)
720 return _org_pull_request_obj, pull_request_obj, \
696 return _org_pull_request_obj, pull_request_obj, \
721 pull_request_display_obj, at_version
697 pull_request_display_obj, at_version
722
698
723 def _get_pr_version_changes(self, version, pull_request_latest):
699 def _get_pr_version_changes(self, version, pull_request_latest):
724 """
700 """
725 Generate changes commits, and diff data based on the current pr version
701 Generate changes commits, and diff data based on the current pr version
726 """
702 """
727
703
728 #TODO(marcink): save those changes as JSON metadata for chaching later.
704 #TODO(marcink): save those changes as JSON metadata for chaching later.
729
705
730 # fake the version to add the "initial" state object
706 # fake the version to add the "initial" state object
731 pull_request_initial = PullRequest.get_pr_display_object(
707 pull_request_initial = PullRequest.get_pr_display_object(
732 pull_request_latest, pull_request_latest,
708 pull_request_latest, pull_request_latest,
733 internal_methods=['get_commit', 'versions'])
709 internal_methods=['get_commit', 'versions'])
734 pull_request_initial.revisions = []
710 pull_request_initial.revisions = []
735 pull_request_initial.source_repo.get_commit = types.MethodType(
711 pull_request_initial.source_repo.get_commit = types.MethodType(
736 lambda *a, **k: EmptyCommit(), pull_request_initial)
712 lambda *a, **k: EmptyCommit(), pull_request_initial)
737 pull_request_initial.source_repo.scm_instance = types.MethodType(
713 pull_request_initial.source_repo.scm_instance = types.MethodType(
738 lambda *a, **k: EmptyRepository(), pull_request_initial)
714 lambda *a, **k: EmptyRepository(), pull_request_initial)
739
715
740 _changes_versions = [pull_request_latest] + \
716 _changes_versions = [pull_request_latest] + \
741 list(reversed(c.versions)) + \
717 list(reversed(c.versions)) + \
742 [pull_request_initial]
718 [pull_request_initial]
743
719
744 if version == 'latest':
720 if version == 'latest':
745 index = 0
721 index = 0
746 else:
722 else:
747 for pos, prver in enumerate(_changes_versions):
723 for pos, prver in enumerate(_changes_versions):
748 ver = getattr(prver, 'pull_request_version_id', -1)
724 ver = getattr(prver, 'pull_request_version_id', -1)
749 if ver == safe_int(version):
725 if ver == safe_int(version):
750 index = pos
726 index = pos
751 break
727 break
752 else:
728 else:
753 index = 0
729 index = 0
754
730
755 cur_obj = _changes_versions[index]
731 cur_obj = _changes_versions[index]
756 prev_obj = _changes_versions[index + 1]
732 prev_obj = _changes_versions[index + 1]
757
733
758 old_commit_ids = set(prev_obj.revisions)
734 old_commit_ids = set(prev_obj.revisions)
759 new_commit_ids = set(cur_obj.revisions)
735 new_commit_ids = set(cur_obj.revisions)
760
736
761 changes = PullRequestModel()._calculate_commit_id_changes(
737 changes = PullRequestModel()._calculate_commit_id_changes(
762 old_commit_ids, new_commit_ids)
738 old_commit_ids, new_commit_ids)
763
739
764 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
740 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
765 cur_obj, prev_obj)
741 cur_obj, prev_obj)
766 file_changes = PullRequestModel()._calculate_file_changes(
742 file_changes = PullRequestModel()._calculate_file_changes(
767 old_diff_data, new_diff_data)
743 old_diff_data, new_diff_data)
768 return changes, file_changes
744 return changes, file_changes
769
745
770 @LoginRequired()
746 @LoginRequired()
771 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
772 'repository.admin')
748 'repository.admin')
773 def show(self, repo_name, pull_request_id):
749 def show(self, repo_name, pull_request_id):
774 pull_request_id = safe_int(pull_request_id)
750 pull_request_id = safe_int(pull_request_id)
775 version = request.GET.get('version')
751 version = request.GET.get('version')
776 merge_checks = request.GET.get('merge_checks')
752 merge_checks = request.GET.get('merge_checks')
777
753
778 (pull_request_latest,
754 (pull_request_latest,
779 pull_request_at_ver,
755 pull_request_at_ver,
780 pull_request_display_obj,
756 pull_request_display_obj,
781 at_version) = self._get_pr_version(pull_request_id, version=version)
757 at_version) = self._get_pr_version(pull_request_id, version=version)
782
758
783 c.template_context['pull_request_data']['pull_request_id'] = \
759 c.template_context['pull_request_data']['pull_request_id'] = \
784 pull_request_id
760 pull_request_id
785
761
786 # pull_requests repo_name we opened it against
762 # pull_requests repo_name we opened it against
787 # ie. target_repo must match
763 # ie. target_repo must match
788 if repo_name != pull_request_at_ver.target_repo.repo_name:
764 if repo_name != pull_request_at_ver.target_repo.repo_name:
789 raise HTTPNotFound
765 raise HTTPNotFound
790
766
791 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
767 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
792 pull_request_at_ver)
768 pull_request_at_ver)
793
769
794 c.ancestor = None # TODO: add ancestor here
770 c.ancestor = None # TODO: add ancestor here
795 c.pull_request = pull_request_display_obj
771 c.pull_request = pull_request_display_obj
796 c.pull_request_latest = pull_request_latest
772 c.pull_request_latest = pull_request_latest
797
773
798 pr_closed = pull_request_latest.is_closed()
774 pr_closed = pull_request_latest.is_closed()
799 if at_version and not at_version == 'latest':
775 if at_version and not at_version == 'latest':
800 c.allowed_to_change_status = False
776 c.allowed_to_change_status = False
801 c.allowed_to_update = False
777 c.allowed_to_update = False
802 c.allowed_to_merge = False
778 c.allowed_to_merge = False
803 c.allowed_to_delete = False
779 c.allowed_to_delete = False
804 c.allowed_to_comment = False
780 c.allowed_to_comment = False
805 else:
781 else:
806 c.allowed_to_change_status = PullRequestModel(). \
782 c.allowed_to_change_status = PullRequestModel(). \
807 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
783 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
808 c.allowed_to_update = PullRequestModel().check_user_update(
784 c.allowed_to_update = PullRequestModel().check_user_update(
809 pull_request_latest, c.rhodecode_user) and not pr_closed
785 pull_request_latest, c.rhodecode_user) and not pr_closed
810 c.allowed_to_merge = PullRequestModel().check_user_merge(
786 c.allowed_to_merge = PullRequestModel().check_user_merge(
811 pull_request_latest, c.rhodecode_user) and not pr_closed
787 pull_request_latest, c.rhodecode_user) and not pr_closed
812 c.allowed_to_delete = PullRequestModel().check_user_delete(
788 c.allowed_to_delete = PullRequestModel().check_user_delete(
813 pull_request_latest, c.rhodecode_user) and not pr_closed
789 pull_request_latest, c.rhodecode_user) and not pr_closed
814 c.allowed_to_comment = not pr_closed
790 c.allowed_to_comment = not pr_closed
815
791
816 cc_model = CommentsModel()
792 cc_model = CommentsModel()
817
793
818 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
794 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
819 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
795 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
820
796
821 c.versions = pull_request_display_obj.versions()
797 c.versions = pull_request_display_obj.versions()
822 c.at_version = at_version
798 c.at_version = at_version
823 c.at_version_num = at_version if at_version and at_version != 'latest' else None
799 c.at_version_num = at_version if at_version and at_version != 'latest' else None
824 c.at_version_pos = ChangesetComment.get_index_from_version(
800 c.at_version_pos = ChangesetComment.get_index_from_version(
825 c.at_version_num, c.versions)
801 c.at_version_num, c.versions)
826
802
827 # GENERAL COMMENTS with versions #
803 # GENERAL COMMENTS with versions #
828 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
804 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
829 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
805 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
830
806
831 # pick comments we want to render at current version
807 # pick comments we want to render at current version
832 c.comment_versions = cc_model.aggregate_comments(
808 c.comment_versions = cc_model.aggregate_comments(
833 general_comments, c.versions, c.at_version_num)
809 general_comments, c.versions, c.at_version_num)
834 c.comments = c.comment_versions[c.at_version_num]['until']
810 c.comments = c.comment_versions[c.at_version_num]['until']
835
811
836 # INLINE COMMENTS with versions #
812 # INLINE COMMENTS with versions #
837 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
813 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
838 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
814 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
839 c.inline_versions = cc_model.aggregate_comments(
815 c.inline_versions = cc_model.aggregate_comments(
840 inline_comments, c.versions, c.at_version_num, inline=True)
816 inline_comments, c.versions, c.at_version_num, inline=True)
841
817
842 # if we use version, then do not show later comments
818 # if we use version, then do not show later comments
843 # than current version
819 # than current version
844 display_inline_comments = collections.defaultdict(lambda: collections.defaultdict(list))
820 display_inline_comments = collections.defaultdict(lambda: collections.defaultdict(list))
845 for co in inline_comments:
821 for co in inline_comments:
846 if c.at_version_num:
822 if c.at_version_num:
847 # pick comments that are at least UPTO given version, so we
823 # pick comments that are at least UPTO given version, so we
848 # don't render comments for higher version
824 # don't render comments for higher version
849 should_render = co.pull_request_version_id and \
825 should_render = co.pull_request_version_id and \
850 co.pull_request_version_id <= c.at_version_num
826 co.pull_request_version_id <= c.at_version_num
851 else:
827 else:
852 # showing all, for 'latest'
828 # showing all, for 'latest'
853 should_render = True
829 should_render = True
854
830
855 if should_render:
831 if should_render:
856 display_inline_comments[co.f_path][co.line_no].append(co)
832 display_inline_comments[co.f_path][co.line_no].append(co)
857
833
858 c.pr_merge_checks = []
834 _merge_check = MergeCheck.validate(
859 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
835 pull_request_latest, user=c.rhodecode_user)
860 pull_request_at_ver)
836 c.pr_merge_errors = _merge_check.errors
861 c.pr_merge_checks.append(['warning' if not c.pr_merge_status else 'success', c.pr_merge_msg])
837 c.pr_merge_possible = not _merge_check.failed
862
838 c.pr_merge_message = _merge_check.merge_msg
863 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
864 approval_msg = _('Reviewer approval is pending.')
865 c.pr_merge_status = False
866 c.pr_merge_checks.append(['warning', approval_msg])
867
868 todos = cc_model.get_unresolved_todos(pull_request_latest)
869 if todos:
870 c.pr_merge_status = False
871 if len(todos) == 1:
872 msg = _('{} todo still not resolved.').format(len(todos))
873 else:
874 msg = _('{} todos still not resolved.').format(len(todos))
875 c.pr_merge_checks.append(['warning', msg])
876
839
877 if merge_checks:
840 if merge_checks:
878 return render('/pullrequests/pullrequest_merge_checks.mako')
841 return render('/pullrequests/pullrequest_merge_checks.mako')
879
842
880 # load compare data into template context
843 # load compare data into template context
881 self._load_compare_data(pull_request_at_ver, display_inline_comments)
844 self._load_compare_data(pull_request_at_ver, display_inline_comments)
882
845
883 # this is a hack to properly display links, when creating PR, the
846 # this is a hack to properly display links, when creating PR, the
884 # compare view and others uses different notation, and
847 # compare view and others uses different notation, and
885 # compare_commits.mako renders links based on the target_repo.
848 # compare_commits.mako renders links based on the target_repo.
886 # We need to swap that here to generate it properly on the html side
849 # We need to swap that here to generate it properly on the html side
887 c.target_repo = c.source_repo
850 c.target_repo = c.source_repo
888
851
889 if c.allowed_to_update:
852 if c.allowed_to_update:
890 force_close = ('forced_closed', _('Close Pull Request'))
853 force_close = ('forced_closed', _('Close Pull Request'))
891 statuses = ChangesetStatus.STATUSES + [force_close]
854 statuses = ChangesetStatus.STATUSES + [force_close]
892 else:
855 else:
893 statuses = ChangesetStatus.STATUSES
856 statuses = ChangesetStatus.STATUSES
894 c.commit_statuses = statuses
857 c.commit_statuses = statuses
895
858
896 c.changes = None
859 c.changes = None
897 c.file_changes = None
860 c.file_changes = None
898
861
899 c.show_version_changes = 1 # control flag, not used yet
862 c.show_version_changes = 1 # control flag, not used yet
900
863
901 if at_version and c.show_version_changes:
864 if at_version and c.show_version_changes:
902 c.changes, c.file_changes = self._get_pr_version_changes(
865 c.changes, c.file_changes = self._get_pr_version_changes(
903 version, pull_request_latest)
866 version, pull_request_latest)
904
867
905 return render('/pullrequests/pullrequest_show.mako')
868 return render('/pullrequests/pullrequest_show.mako')
906
869
907 @LoginRequired()
870 @LoginRequired()
908 @NotAnonymous()
871 @NotAnonymous()
909 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
872 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
910 'repository.admin')
873 'repository.admin')
911 @auth.CSRFRequired()
874 @auth.CSRFRequired()
912 @jsonify
875 @jsonify
913 def comment(self, repo_name, pull_request_id):
876 def comment(self, repo_name, pull_request_id):
914 pull_request_id = safe_int(pull_request_id)
877 pull_request_id = safe_int(pull_request_id)
915 pull_request = PullRequest.get_or_404(pull_request_id)
878 pull_request = PullRequest.get_or_404(pull_request_id)
916 if pull_request.is_closed():
879 if pull_request.is_closed():
917 raise HTTPForbidden()
880 raise HTTPForbidden()
918
881
919 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
882 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
920 # as a changeset status, still we want to send it in one value.
883 # as a changeset status, still we want to send it in one value.
921 status = request.POST.get('changeset_status', None)
884 status = request.POST.get('changeset_status', None)
922 text = request.POST.get('text')
885 text = request.POST.get('text')
923 comment_type = request.POST.get('comment_type')
886 comment_type = request.POST.get('comment_type')
924 resolves_comment_id = request.POST.get('resolves_comment_id', None)
887 resolves_comment_id = request.POST.get('resolves_comment_id', None)
925
888
926 if status and '_closed' in status:
889 if status and '_closed' in status:
927 close_pr = True
890 close_pr = True
928 status = status.replace('_closed', '')
891 status = status.replace('_closed', '')
929 else:
892 else:
930 close_pr = False
893 close_pr = False
931
894
932 forced = (status == 'forced')
895 forced = (status == 'forced')
933 if forced:
896 if forced:
934 status = 'rejected'
897 status = 'rejected'
935
898
936 allowed_to_change_status = PullRequestModel().check_user_change_status(
899 allowed_to_change_status = PullRequestModel().check_user_change_status(
937 pull_request, c.rhodecode_user)
900 pull_request, c.rhodecode_user)
938
901
939 if status and allowed_to_change_status:
902 if status and allowed_to_change_status:
940 message = (_('Status change %(transition_icon)s %(status)s')
903 message = (_('Status change %(transition_icon)s %(status)s')
941 % {'transition_icon': '>',
904 % {'transition_icon': '>',
942 'status': ChangesetStatus.get_status_lbl(status)})
905 'status': ChangesetStatus.get_status_lbl(status)})
943 if close_pr:
906 if close_pr:
944 message = _('Closing with') + ' ' + message
907 message = _('Closing with') + ' ' + message
945 text = text or message
908 text = text or message
946 comm = CommentsModel().create(
909 comm = CommentsModel().create(
947 text=text,
910 text=text,
948 repo=c.rhodecode_db_repo.repo_id,
911 repo=c.rhodecode_db_repo.repo_id,
949 user=c.rhodecode_user.user_id,
912 user=c.rhodecode_user.user_id,
950 pull_request=pull_request_id,
913 pull_request=pull_request_id,
951 f_path=request.POST.get('f_path'),
914 f_path=request.POST.get('f_path'),
952 line_no=request.POST.get('line'),
915 line_no=request.POST.get('line'),
953 status_change=(ChangesetStatus.get_status_lbl(status)
916 status_change=(ChangesetStatus.get_status_lbl(status)
954 if status and allowed_to_change_status else None),
917 if status and allowed_to_change_status else None),
955 status_change_type=(status
918 status_change_type=(status
956 if status and allowed_to_change_status else None),
919 if status and allowed_to_change_status else None),
957 closing_pr=close_pr,
920 closing_pr=close_pr,
958 comment_type=comment_type,
921 comment_type=comment_type,
959 resolves_comment_id=resolves_comment_id
922 resolves_comment_id=resolves_comment_id
960 )
923 )
961
924
962 if allowed_to_change_status:
925 if allowed_to_change_status:
963 old_calculated_status = pull_request.calculated_review_status()
926 old_calculated_status = pull_request.calculated_review_status()
964 # get status if set !
927 # get status if set !
965 if status:
928 if status:
966 ChangesetStatusModel().set_status(
929 ChangesetStatusModel().set_status(
967 c.rhodecode_db_repo.repo_id,
930 c.rhodecode_db_repo.repo_id,
968 status,
931 status,
969 c.rhodecode_user.user_id,
932 c.rhodecode_user.user_id,
970 comm,
933 comm,
971 pull_request=pull_request_id
934 pull_request=pull_request_id
972 )
935 )
973
936
974 Session().flush()
937 Session().flush()
975 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
938 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
976 # we now calculate the status of pull request, and based on that
939 # we now calculate the status of pull request, and based on that
977 # calculation we set the commits status
940 # calculation we set the commits status
978 calculated_status = pull_request.calculated_review_status()
941 calculated_status = pull_request.calculated_review_status()
979 if old_calculated_status != calculated_status:
942 if old_calculated_status != calculated_status:
980 PullRequestModel()._trigger_pull_request_hook(
943 PullRequestModel()._trigger_pull_request_hook(
981 pull_request, c.rhodecode_user, 'review_status_change')
944 pull_request, c.rhodecode_user, 'review_status_change')
982
945
983 calculated_status_lbl = ChangesetStatus.get_status_lbl(
946 calculated_status_lbl = ChangesetStatus.get_status_lbl(
984 calculated_status)
947 calculated_status)
985
948
986 if close_pr:
949 if close_pr:
987 status_completed = (
950 status_completed = (
988 calculated_status in [ChangesetStatus.STATUS_APPROVED,
951 calculated_status in [ChangesetStatus.STATUS_APPROVED,
989 ChangesetStatus.STATUS_REJECTED])
952 ChangesetStatus.STATUS_REJECTED])
990 if forced or status_completed:
953 if forced or status_completed:
991 PullRequestModel().close_pull_request(
954 PullRequestModel().close_pull_request(
992 pull_request_id, c.rhodecode_user)
955 pull_request_id, c.rhodecode_user)
993 else:
956 else:
994 h.flash(_('Closing pull request on other statuses than '
957 h.flash(_('Closing pull request on other statuses than '
995 'rejected or approved is forbidden. '
958 'rejected or approved is forbidden. '
996 'Calculated status from all reviewers '
959 'Calculated status from all reviewers '
997 'is currently: %s') % calculated_status_lbl,
960 'is currently: %s') % calculated_status_lbl,
998 category='warning')
961 category='warning')
999
962
1000 Session().commit()
963 Session().commit()
1001
964
1002 if not request.is_xhr:
965 if not request.is_xhr:
1003 return redirect(h.url('pullrequest_show', repo_name=repo_name,
966 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1004 pull_request_id=pull_request_id))
967 pull_request_id=pull_request_id))
1005
968
1006 data = {
969 data = {
1007 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
970 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1008 }
971 }
1009 if comm:
972 if comm:
1010 c.co = comm
973 c.co = comm
1011 c.inline_comment = True if comm.line_no else False
974 c.inline_comment = True if comm.line_no else False
1012 data.update(comm.get_dict())
975 data.update(comm.get_dict())
1013 data.update({'rendered_text':
976 data.update({'rendered_text':
1014 render('changeset/changeset_comment_block.mako')})
977 render('changeset/changeset_comment_block.mako')})
1015
978
1016 return data
979 return data
1017
980
1018 @LoginRequired()
981 @LoginRequired()
1019 @NotAnonymous()
982 @NotAnonymous()
1020 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
983 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1021 'repository.admin')
984 'repository.admin')
1022 @auth.CSRFRequired()
985 @auth.CSRFRequired()
1023 @jsonify
986 @jsonify
1024 def delete_comment(self, repo_name, comment_id):
987 def delete_comment(self, repo_name, comment_id):
1025 return self._delete_comment(comment_id)
988 return self._delete_comment(comment_id)
1026
989
1027 def _delete_comment(self, comment_id):
990 def _delete_comment(self, comment_id):
1028 comment_id = safe_int(comment_id)
991 comment_id = safe_int(comment_id)
1029 co = ChangesetComment.get_or_404(comment_id)
992 co = ChangesetComment.get_or_404(comment_id)
1030 if co.pull_request.is_closed():
993 if co.pull_request.is_closed():
1031 # don't allow deleting comments on closed pull request
994 # don't allow deleting comments on closed pull request
1032 raise HTTPForbidden()
995 raise HTTPForbidden()
1033
996
1034 is_owner = co.author.user_id == c.rhodecode_user.user_id
997 is_owner = co.author.user_id == c.rhodecode_user.user_id
1035 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
998 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1036 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
999 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1037 old_calculated_status = co.pull_request.calculated_review_status()
1000 old_calculated_status = co.pull_request.calculated_review_status()
1038 CommentsModel().delete(comment=co)
1001 CommentsModel().delete(comment=co)
1039 Session().commit()
1002 Session().commit()
1040 calculated_status = co.pull_request.calculated_review_status()
1003 calculated_status = co.pull_request.calculated_review_status()
1041 if old_calculated_status != calculated_status:
1004 if old_calculated_status != calculated_status:
1042 PullRequestModel()._trigger_pull_request_hook(
1005 PullRequestModel()._trigger_pull_request_hook(
1043 co.pull_request, c.rhodecode_user, 'review_status_change')
1006 co.pull_request, c.rhodecode_user, 'review_status_change')
1044 return True
1007 return True
1045 else:
1008 else:
1046 raise HTTPForbidden()
1009 raise HTTPForbidden()
@@ -1,1318 +1,1398 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 from collections import namedtuple
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
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 from sqlalchemy import or_
35
35
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 from rhodecode.lib.markup_renderer import (
39 from rhodecode.lib.markup_renderer import (
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils import action_logger
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 CommitDoesNotExistError, EmptyRepositoryError)
47 CommitDoesNotExistError, EmptyRepositoryError)
48 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 PullRequestVersion, ChangesetComment)
53 PullRequestVersion, ChangesetComment)
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.notification import NotificationModel, \
55 from rhodecode.model.notification import NotificationModel, \
56 EmailNotificationModel
56 EmailNotificationModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.settings import VcsSettingsModel
58 from rhodecode.model.settings import VcsSettingsModel
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 # Data structure to hold the response data when updating commits during a pull
64 # Data structure to hold the response data when updating commits during a pull
65 # request update.
65 # request update.
66 UpdateResponse = namedtuple(
66 UpdateResponse = namedtuple(
67 'UpdateResponse', 'executed, reason, new, old, changes')
67 'UpdateResponse', 'executed, reason, new, old, changes')
68
68
69
69
70 class PullRequestModel(BaseModel):
70 class PullRequestModel(BaseModel):
71
71
72 cls = PullRequest
72 cls = PullRequest
73
73
74 DIFF_CONTEXT = 3
74 DIFF_CONTEXT = 3
75
75
76 MERGE_STATUS_MESSAGES = {
76 MERGE_STATUS_MESSAGES = {
77 MergeFailureReason.NONE: lazy_ugettext(
77 MergeFailureReason.NONE: lazy_ugettext(
78 'This pull request can be automatically merged.'),
78 'This pull request can be automatically merged.'),
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 'This pull request cannot be merged because of an unhandled'
80 'This pull request cannot be merged because of an unhandled'
81 ' exception.'),
81 ' exception.'),
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 'This pull request cannot be merged because of conflicts.'),
83 'This pull request cannot be merged because of conflicts.'),
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 'This pull request could not be merged because push to target'
85 'This pull request could not be merged because push to target'
86 ' failed.'),
86 ' failed.'),
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 'This pull request cannot be merged because the target is not a'
88 'This pull request cannot be merged because the target is not a'
89 ' head.'),
89 ' head.'),
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 'This pull request cannot be merged because the source contains'
91 'This pull request cannot be merged because the source contains'
92 ' more branches than the target.'),
92 ' more branches than the target.'),
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 'This pull request cannot be merged because the target has'
94 'This pull request cannot be merged because the target has'
95 ' multiple heads.'),
95 ' multiple heads.'),
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 'This pull request cannot be merged because the target repository'
97 'This pull request cannot be merged because the target repository'
98 ' is locked.'),
98 ' is locked.'),
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 'This pull request cannot be merged because the target or the '
100 'This pull request cannot be merged because the target or the '
101 'source reference is missing.'),
101 'source reference is missing.'),
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 'This pull request cannot be merged because the target '
103 'This pull request cannot be merged because the target '
104 'reference is missing.'),
104 'reference is missing.'),
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 'This pull request cannot be merged because the source '
106 'This pull request cannot be merged because the source '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 'This pull request cannot be merged because of conflicts related '
109 'This pull request cannot be merged because of conflicts related '
110 'to sub repositories.'),
110 'to sub repositories.'),
111 }
111 }
112
112
113 UPDATE_STATUS_MESSAGES = {
113 UPDATE_STATUS_MESSAGES = {
114 UpdateFailureReason.NONE: lazy_ugettext(
114 UpdateFailureReason.NONE: lazy_ugettext(
115 'Pull request update successful.'),
115 'Pull request update successful.'),
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 'Pull request update failed because of an unknown error.'),
117 'Pull request update failed because of an unknown error.'),
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 'No update needed because the source reference is already '
119 'No update needed because the source reference is already '
120 'up to date.'),
120 'up to date.'),
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
122 'Pull request cannot be updated because the reference type is '
122 'Pull request cannot be updated because the reference type is '
123 'not supported for an update.'),
123 'not supported for an update.'),
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 'This pull request cannot be updated because the target '
125 'This pull request cannot be updated because the target '
126 'reference is missing.'),
126 'reference is missing.'),
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 'This pull request cannot be updated because the source '
128 'This pull request cannot be updated because the source '
129 'reference is missing.'),
129 'reference is missing.'),
130 }
130 }
131
131
132 def __get_pull_request(self, pull_request):
132 def __get_pull_request(self, pull_request):
133 return self._get_instance((
133 return self._get_instance((
134 PullRequest, PullRequestVersion), pull_request)
134 PullRequest, PullRequestVersion), pull_request)
135
135
136 def _check_perms(self, perms, pull_request, user, api=False):
136 def _check_perms(self, perms, pull_request, user, api=False):
137 if not api:
137 if not api:
138 return h.HasRepoPermissionAny(*perms)(
138 return h.HasRepoPermissionAny(*perms)(
139 user=user, repo_name=pull_request.target_repo.repo_name)
139 user=user, repo_name=pull_request.target_repo.repo_name)
140 else:
140 else:
141 return h.HasRepoPermissionAnyApi(*perms)(
141 return h.HasRepoPermissionAnyApi(*perms)(
142 user=user, repo_name=pull_request.target_repo.repo_name)
142 user=user, repo_name=pull_request.target_repo.repo_name)
143
143
144 def check_user_read(self, pull_request, user, api=False):
144 def check_user_read(self, pull_request, user, api=False):
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 return self._check_perms(_perms, pull_request, user, api)
146 return self._check_perms(_perms, pull_request, user, api)
147
147
148 def check_user_merge(self, pull_request, user, api=False):
148 def check_user_merge(self, pull_request, user, api=False):
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 return self._check_perms(_perms, pull_request, user, api)
150 return self._check_perms(_perms, pull_request, user, api)
151
151
152 def check_user_update(self, pull_request, user, api=False):
152 def check_user_update(self, pull_request, user, api=False):
153 owner = user.user_id == pull_request.user_id
153 owner = user.user_id == pull_request.user_id
154 return self.check_user_merge(pull_request, user, api) or owner
154 return self.check_user_merge(pull_request, user, api) or owner
155
155
156 def check_user_delete(self, pull_request, user):
156 def check_user_delete(self, pull_request, user):
157 owner = user.user_id == pull_request.user_id
157 owner = user.user_id == pull_request.user_id
158 _perms = ('repository.admin')
158 _perms = ('repository.admin')
159 return self._check_perms(_perms, pull_request, user) or owner
159 return self._check_perms(_perms, pull_request, user) or owner
160
160
161 def check_user_change_status(self, pull_request, user, api=False):
161 def check_user_change_status(self, pull_request, user, api=False):
162 reviewer = user.user_id in [x.user_id for x in
162 reviewer = user.user_id in [x.user_id for x in
163 pull_request.reviewers]
163 pull_request.reviewers]
164 return self.check_user_update(pull_request, user, api) or reviewer
164 return self.check_user_update(pull_request, user, api) or reviewer
165
165
166 def get(self, pull_request):
166 def get(self, pull_request):
167 return self.__get_pull_request(pull_request)
167 return self.__get_pull_request(pull_request)
168
168
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 opened_by=None, order_by=None,
170 opened_by=None, order_by=None,
171 order_dir='desc'):
171 order_dir='desc'):
172 repo = None
172 repo = None
173 if repo_name:
173 if repo_name:
174 repo = self._get_repo(repo_name)
174 repo = self._get_repo(repo_name)
175
175
176 q = PullRequest.query()
176 q = PullRequest.query()
177
177
178 # source or target
178 # source or target
179 if repo and source:
179 if repo and source:
180 q = q.filter(PullRequest.source_repo == repo)
180 q = q.filter(PullRequest.source_repo == repo)
181 elif repo:
181 elif repo:
182 q = q.filter(PullRequest.target_repo == repo)
182 q = q.filter(PullRequest.target_repo == repo)
183
183
184 # closed,opened
184 # closed,opened
185 if statuses:
185 if statuses:
186 q = q.filter(PullRequest.status.in_(statuses))
186 q = q.filter(PullRequest.status.in_(statuses))
187
187
188 # opened by filter
188 # opened by filter
189 if opened_by:
189 if opened_by:
190 q = q.filter(PullRequest.user_id.in_(opened_by))
190 q = q.filter(PullRequest.user_id.in_(opened_by))
191
191
192 if order_by:
192 if order_by:
193 order_map = {
193 order_map = {
194 'name_raw': PullRequest.pull_request_id,
194 'name_raw': PullRequest.pull_request_id,
195 'title': PullRequest.title,
195 'title': PullRequest.title,
196 'updated_on_raw': PullRequest.updated_on,
196 'updated_on_raw': PullRequest.updated_on,
197 'target_repo': PullRequest.target_repo_id
197 'target_repo': PullRequest.target_repo_id
198 }
198 }
199 if order_dir == 'asc':
199 if order_dir == 'asc':
200 q = q.order_by(order_map[order_by].asc())
200 q = q.order_by(order_map[order_by].asc())
201 else:
201 else:
202 q = q.order_by(order_map[order_by].desc())
202 q = q.order_by(order_map[order_by].desc())
203
203
204 return q
204 return q
205
205
206 def count_all(self, repo_name, source=False, statuses=None,
206 def count_all(self, repo_name, source=False, statuses=None,
207 opened_by=None):
207 opened_by=None):
208 """
208 """
209 Count the number of pull requests for a specific repository.
209 Count the number of pull requests for a specific repository.
210
210
211 :param repo_name: target or source repo
211 :param repo_name: target or source repo
212 :param source: boolean flag to specify if repo_name refers to source
212 :param source: boolean flag to specify if repo_name refers to source
213 :param statuses: list of pull request statuses
213 :param statuses: list of pull request statuses
214 :param opened_by: author user of the pull request
214 :param opened_by: author user of the pull request
215 :returns: int number of pull requests
215 :returns: int number of pull requests
216 """
216 """
217 q = self._prepare_get_all_query(
217 q = self._prepare_get_all_query(
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219
219
220 return q.count()
220 return q.count()
221
221
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 offset=0, length=None, order_by=None, order_dir='desc'):
223 offset=0, length=None, order_by=None, order_dir='desc'):
224 """
224 """
225 Get all pull requests for a specific repository.
225 Get all pull requests for a specific repository.
226
226
227 :param repo_name: target or source repo
227 :param repo_name: target or source repo
228 :param source: boolean flag to specify if repo_name refers to source
228 :param source: boolean flag to specify if repo_name refers to source
229 :param statuses: list of pull request statuses
229 :param statuses: list of pull request statuses
230 :param opened_by: author user of the pull request
230 :param opened_by: author user of the pull request
231 :param offset: pagination offset
231 :param offset: pagination offset
232 :param length: length of returned list
232 :param length: length of returned list
233 :param order_by: order of the returned list
233 :param order_by: order of the returned list
234 :param order_dir: 'asc' or 'desc' ordering direction
234 :param order_dir: 'asc' or 'desc' ordering direction
235 :returns: list of pull requests
235 :returns: list of pull requests
236 """
236 """
237 q = self._prepare_get_all_query(
237 q = self._prepare_get_all_query(
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 order_by=order_by, order_dir=order_dir)
239 order_by=order_by, order_dir=order_dir)
240
240
241 if length:
241 if length:
242 pull_requests = q.limit(length).offset(offset).all()
242 pull_requests = q.limit(length).offset(offset).all()
243 else:
243 else:
244 pull_requests = q.all()
244 pull_requests = q.all()
245
245
246 return pull_requests
246 return pull_requests
247
247
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 opened_by=None):
249 opened_by=None):
250 """
250 """
251 Count the number of pull requests for a specific repository that are
251 Count the number of pull requests for a specific repository that are
252 awaiting review.
252 awaiting review.
253
253
254 :param repo_name: target or source repo
254 :param repo_name: target or source repo
255 :param source: boolean flag to specify if repo_name refers to source
255 :param source: boolean flag to specify if repo_name refers to source
256 :param statuses: list of pull request statuses
256 :param statuses: list of pull request statuses
257 :param opened_by: author user of the pull request
257 :param opened_by: author user of the pull request
258 :returns: int number of pull requests
258 :returns: int number of pull requests
259 """
259 """
260 pull_requests = self.get_awaiting_review(
260 pull_requests = self.get_awaiting_review(
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262
262
263 return len(pull_requests)
263 return len(pull_requests)
264
264
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 opened_by=None, offset=0, length=None,
266 opened_by=None, offset=0, length=None,
267 order_by=None, order_dir='desc'):
267 order_by=None, order_dir='desc'):
268 """
268 """
269 Get all pull requests for a specific repository that are awaiting
269 Get all pull requests for a specific repository that are awaiting
270 review.
270 review.
271
271
272 :param repo_name: target or source repo
272 :param repo_name: target or source repo
273 :param source: boolean flag to specify if repo_name refers to source
273 :param source: boolean flag to specify if repo_name refers to source
274 :param statuses: list of pull request statuses
274 :param statuses: list of pull request statuses
275 :param opened_by: author user of the pull request
275 :param opened_by: author user of the pull request
276 :param offset: pagination offset
276 :param offset: pagination offset
277 :param length: length of returned list
277 :param length: length of returned list
278 :param order_by: order of the returned list
278 :param order_by: order of the returned list
279 :param order_dir: 'asc' or 'desc' ordering direction
279 :param order_dir: 'asc' or 'desc' ordering direction
280 :returns: list of pull requests
280 :returns: list of pull requests
281 """
281 """
282 pull_requests = self.get_all(
282 pull_requests = self.get_all(
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 order_by=order_by, order_dir=order_dir)
284 order_by=order_by, order_dir=order_dir)
285
285
286 _filtered_pull_requests = []
286 _filtered_pull_requests = []
287 for pr in pull_requests:
287 for pr in pull_requests:
288 status = pr.calculated_review_status()
288 status = pr.calculated_review_status()
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 _filtered_pull_requests.append(pr)
291 _filtered_pull_requests.append(pr)
292 if length:
292 if length:
293 return _filtered_pull_requests[offset:offset+length]
293 return _filtered_pull_requests[offset:offset+length]
294 else:
294 else:
295 return _filtered_pull_requests
295 return _filtered_pull_requests
296
296
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 opened_by=None, user_id=None):
298 opened_by=None, user_id=None):
299 """
299 """
300 Count the number of pull requests for a specific repository that are
300 Count the number of pull requests for a specific repository that are
301 awaiting review from a specific user.
301 awaiting review from a specific user.
302
302
303 :param repo_name: target or source repo
303 :param repo_name: target or source repo
304 :param source: boolean flag to specify if repo_name refers to source
304 :param source: boolean flag to specify if repo_name refers to source
305 :param statuses: list of pull request statuses
305 :param statuses: list of pull request statuses
306 :param opened_by: author user of the pull request
306 :param opened_by: author user of the pull request
307 :param user_id: reviewer user of the pull request
307 :param user_id: reviewer user of the pull request
308 :returns: int number of pull requests
308 :returns: int number of pull requests
309 """
309 """
310 pull_requests = self.get_awaiting_my_review(
310 pull_requests = self.get_awaiting_my_review(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 user_id=user_id)
312 user_id=user_id)
313
313
314 return len(pull_requests)
314 return len(pull_requests)
315
315
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 opened_by=None, user_id=None, offset=0,
317 opened_by=None, user_id=None, offset=0,
318 length=None, order_by=None, order_dir='desc'):
318 length=None, order_by=None, order_dir='desc'):
319 """
319 """
320 Get all pull requests for a specific repository that are awaiting
320 Get all pull requests for a specific repository that are awaiting
321 review from a specific user.
321 review from a specific user.
322
322
323 :param repo_name: target or source repo
323 :param repo_name: target or source repo
324 :param source: boolean flag to specify if repo_name refers to source
324 :param source: boolean flag to specify if repo_name refers to source
325 :param statuses: list of pull request statuses
325 :param statuses: list of pull request statuses
326 :param opened_by: author user of the pull request
326 :param opened_by: author user of the pull request
327 :param user_id: reviewer user of the pull request
327 :param user_id: reviewer user of the pull request
328 :param offset: pagination offset
328 :param offset: pagination offset
329 :param length: length of returned list
329 :param length: length of returned list
330 :param order_by: order of the returned list
330 :param order_by: order of the returned list
331 :param order_dir: 'asc' or 'desc' ordering direction
331 :param order_dir: 'asc' or 'desc' ordering direction
332 :returns: list of pull requests
332 :returns: list of pull requests
333 """
333 """
334 pull_requests = self.get_all(
334 pull_requests = self.get_all(
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 order_by=order_by, order_dir=order_dir)
336 order_by=order_by, order_dir=order_dir)
337
337
338 _my = PullRequestModel().get_not_reviewed(user_id)
338 _my = PullRequestModel().get_not_reviewed(user_id)
339 my_participation = []
339 my_participation = []
340 for pr in pull_requests:
340 for pr in pull_requests:
341 if pr in _my:
341 if pr in _my:
342 my_participation.append(pr)
342 my_participation.append(pr)
343 _filtered_pull_requests = my_participation
343 _filtered_pull_requests = my_participation
344 if length:
344 if length:
345 return _filtered_pull_requests[offset:offset+length]
345 return _filtered_pull_requests[offset:offset+length]
346 else:
346 else:
347 return _filtered_pull_requests
347 return _filtered_pull_requests
348
348
349 def get_not_reviewed(self, user_id):
349 def get_not_reviewed(self, user_id):
350 return [
350 return [
351 x.pull_request for x in PullRequestReviewers.query().filter(
351 x.pull_request for x in PullRequestReviewers.query().filter(
352 PullRequestReviewers.user_id == user_id).all()
352 PullRequestReviewers.user_id == user_id).all()
353 ]
353 ]
354
354
355 def _prepare_participating_query(self, user_id=None, statuses=None,
355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 order_by=None, order_dir='desc'):
356 order_by=None, order_dir='desc'):
357 q = PullRequest.query()
357 q = PullRequest.query()
358 if user_id:
358 if user_id:
359 reviewers_subquery = Session().query(
359 reviewers_subquery = Session().query(
360 PullRequestReviewers.pull_request_id).filter(
360 PullRequestReviewers.pull_request_id).filter(
361 PullRequestReviewers.user_id == user_id).subquery()
361 PullRequestReviewers.user_id == user_id).subquery()
362 user_filter= or_(
362 user_filter= or_(
363 PullRequest.user_id == user_id,
363 PullRequest.user_id == user_id,
364 PullRequest.pull_request_id.in_(reviewers_subquery)
364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 )
365 )
366 q = PullRequest.query().filter(user_filter)
366 q = PullRequest.query().filter(user_filter)
367
367
368 # closed,opened
368 # closed,opened
369 if statuses:
369 if statuses:
370 q = q.filter(PullRequest.status.in_(statuses))
370 q = q.filter(PullRequest.status.in_(statuses))
371
371
372 if order_by:
372 if order_by:
373 order_map = {
373 order_map = {
374 'name_raw': PullRequest.pull_request_id,
374 'name_raw': PullRequest.pull_request_id,
375 'title': PullRequest.title,
375 'title': PullRequest.title,
376 'updated_on_raw': PullRequest.updated_on,
376 'updated_on_raw': PullRequest.updated_on,
377 'target_repo': PullRequest.target_repo_id
377 'target_repo': PullRequest.target_repo_id
378 }
378 }
379 if order_dir == 'asc':
379 if order_dir == 'asc':
380 q = q.order_by(order_map[order_by].asc())
380 q = q.order_by(order_map[order_by].asc())
381 else:
381 else:
382 q = q.order_by(order_map[order_by].desc())
382 q = q.order_by(order_map[order_by].desc())
383
383
384 return q
384 return q
385
385
386 def count_im_participating_in(self, user_id=None, statuses=None):
386 def count_im_participating_in(self, user_id=None, statuses=None):
387 q = self._prepare_participating_query(user_id, statuses=statuses)
387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 return q.count()
388 return q.count()
389
389
390 def get_im_participating_in(
390 def get_im_participating_in(
391 self, user_id=None, statuses=None, offset=0,
391 self, user_id=None, statuses=None, offset=0,
392 length=None, order_by=None, order_dir='desc'):
392 length=None, order_by=None, order_dir='desc'):
393 """
393 """
394 Get all Pull requests that i'm participating in, or i have opened
394 Get all Pull requests that i'm participating in, or i have opened
395 """
395 """
396
396
397 q = self._prepare_participating_query(
397 q = self._prepare_participating_query(
398 user_id, statuses=statuses, order_by=order_by,
398 user_id, statuses=statuses, order_by=order_by,
399 order_dir=order_dir)
399 order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def get_versions(self, pull_request):
408 def get_versions(self, pull_request):
409 """
409 """
410 returns version of pull request sorted by ID descending
410 returns version of pull request sorted by ID descending
411 """
411 """
412 return PullRequestVersion.query()\
412 return PullRequestVersion.query()\
413 .filter(PullRequestVersion.pull_request == pull_request)\
413 .filter(PullRequestVersion.pull_request == pull_request)\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 .all()
415 .all()
416
416
417 def create(self, created_by, source_repo, source_ref, target_repo,
417 def create(self, created_by, source_repo, source_ref, target_repo,
418 target_ref, revisions, reviewers, title, description=None):
418 target_ref, revisions, reviewers, title, description=None):
419 created_by_user = self._get_user(created_by)
419 created_by_user = self._get_user(created_by)
420 source_repo = self._get_repo(source_repo)
420 source_repo = self._get_repo(source_repo)
421 target_repo = self._get_repo(target_repo)
421 target_repo = self._get_repo(target_repo)
422
422
423 pull_request = PullRequest()
423 pull_request = PullRequest()
424 pull_request.source_repo = source_repo
424 pull_request.source_repo = source_repo
425 pull_request.source_ref = source_ref
425 pull_request.source_ref = source_ref
426 pull_request.target_repo = target_repo
426 pull_request.target_repo = target_repo
427 pull_request.target_ref = target_ref
427 pull_request.target_ref = target_ref
428 pull_request.revisions = revisions
428 pull_request.revisions = revisions
429 pull_request.title = title
429 pull_request.title = title
430 pull_request.description = description
430 pull_request.description = description
431 pull_request.author = created_by_user
431 pull_request.author = created_by_user
432
432
433 Session().add(pull_request)
433 Session().add(pull_request)
434 Session().flush()
434 Session().flush()
435
435
436 reviewer_ids = set()
436 reviewer_ids = set()
437 # members / reviewers
437 # members / reviewers
438 for reviewer_object in reviewers:
438 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
439 if isinstance(reviewer_object, tuple):
440 user_id, reasons = reviewer_object
440 user_id, reasons = reviewer_object
441 else:
441 else:
442 user_id, reasons = reviewer_object, []
442 user_id, reasons = reviewer_object, []
443
443
444 user = self._get_user(user_id)
444 user = self._get_user(user_id)
445 reviewer_ids.add(user.user_id)
445 reviewer_ids.add(user.user_id)
446
446
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
448 Session().add(reviewer)
448 Session().add(reviewer)
449
449
450 # Set approval status to "Under Review" for all commits which are
450 # Set approval status to "Under Review" for all commits which are
451 # part of this pull request.
451 # part of this pull request.
452 ChangesetStatusModel().set_status(
452 ChangesetStatusModel().set_status(
453 repo=target_repo,
453 repo=target_repo,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 user=created_by_user,
455 user=created_by_user,
456 pull_request=pull_request
456 pull_request=pull_request
457 )
457 )
458
458
459 self.notify_reviewers(pull_request, reviewer_ids)
459 self.notify_reviewers(pull_request, reviewer_ids)
460 self._trigger_pull_request_hook(
460 self._trigger_pull_request_hook(
461 pull_request, created_by_user, 'create')
461 pull_request, created_by_user, 'create')
462
462
463 return pull_request
463 return pull_request
464
464
465 def _trigger_pull_request_hook(self, pull_request, user, action):
465 def _trigger_pull_request_hook(self, pull_request, user, action):
466 pull_request = self.__get_pull_request(pull_request)
466 pull_request = self.__get_pull_request(pull_request)
467 target_scm = pull_request.target_repo.scm_instance()
467 target_scm = pull_request.target_repo.scm_instance()
468 if action == 'create':
468 if action == 'create':
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 elif action == 'merge':
470 elif action == 'merge':
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 elif action == 'close':
472 elif action == 'close':
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 elif action == 'review_status_change':
474 elif action == 'review_status_change':
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 elif action == 'update':
476 elif action == 'update':
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 else:
478 else:
479 return
479 return
480
480
481 trigger_hook(
481 trigger_hook(
482 username=user.username,
482 username=user.username,
483 repo_name=pull_request.target_repo.repo_name,
483 repo_name=pull_request.target_repo.repo_name,
484 repo_alias=target_scm.alias,
484 repo_alias=target_scm.alias,
485 pull_request=pull_request)
485 pull_request=pull_request)
486
486
487 def _get_commit_ids(self, pull_request):
487 def _get_commit_ids(self, pull_request):
488 """
488 """
489 Return the commit ids of the merged pull request.
489 Return the commit ids of the merged pull request.
490
490
491 This method is not dealing correctly yet with the lack of autoupdates
491 This method is not dealing correctly yet with the lack of autoupdates
492 nor with the implicit target updates.
492 nor with the implicit target updates.
493 For example: if a commit in the source repo is already in the target it
493 For example: if a commit in the source repo is already in the target it
494 will be reported anyways.
494 will be reported anyways.
495 """
495 """
496 merge_rev = pull_request.merge_rev
496 merge_rev = pull_request.merge_rev
497 if merge_rev is None:
497 if merge_rev is None:
498 raise ValueError('This pull request was not merged yet')
498 raise ValueError('This pull request was not merged yet')
499
499
500 commit_ids = list(pull_request.revisions)
500 commit_ids = list(pull_request.revisions)
501 if merge_rev not in commit_ids:
501 if merge_rev not in commit_ids:
502 commit_ids.append(merge_rev)
502 commit_ids.append(merge_rev)
503
503
504 return commit_ids
504 return commit_ids
505
505
506 def merge(self, pull_request, user, extras):
506 def merge(self, pull_request, user, extras):
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
509 if merge_state.executed:
509 if merge_state.executed:
510 log.debug(
510 log.debug(
511 "Merge was successful, updating the pull request comments.")
511 "Merge was successful, updating the pull request comments.")
512 self._comment_and_close_pr(pull_request, user, merge_state)
512 self._comment_and_close_pr(pull_request, user, merge_state)
513 self._log_action('user_merged_pull_request', user, pull_request)
513 self._log_action('user_merged_pull_request', user, pull_request)
514 else:
514 else:
515 log.warn("Merge failed, not updating the pull request.")
515 log.warn("Merge failed, not updating the pull request.")
516 return merge_state
516 return merge_state
517
517
518 def _merge_pull_request(self, pull_request, user, extras):
518 def _merge_pull_request(self, pull_request, user, extras):
519 target_vcs = pull_request.target_repo.scm_instance()
519 target_vcs = pull_request.target_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
521 target_ref = self._refresh_reference(
521 target_ref = self._refresh_reference(
522 pull_request.target_ref_parts, target_vcs)
522 pull_request.target_ref_parts, target_vcs)
523
523
524 message = _(
524 message = _(
525 'Merge pull request #%(pr_id)s from '
525 'Merge pull request #%(pr_id)s from '
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 'pr_id': pull_request.pull_request_id,
527 'pr_id': pull_request.pull_request_id,
528 'source_repo': source_vcs.name,
528 'source_repo': source_vcs.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
530 'pr_title': pull_request.title
530 'pr_title': pull_request.title
531 }
531 }
532
532
533 workspace_id = self._workspace_id(pull_request)
533 workspace_id = self._workspace_id(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
535
535
536 callback_daemon, extras = prepare_callback_daemon(
536 callback_daemon, extras = prepare_callback_daemon(
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539
539
540 with callback_daemon:
540 with callback_daemon:
541 # TODO: johbo: Implement a clean way to run a config_override
541 # TODO: johbo: Implement a clean way to run a config_override
542 # for a single call.
542 # for a single call.
543 target_vcs.config.set(
543 target_vcs.config.set(
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 merge_state = target_vcs.merge(
545 merge_state = target_vcs.merge(
546 target_ref, source_vcs, pull_request.source_ref_parts,
546 target_ref, source_vcs, pull_request.source_ref_parts,
547 workspace_id, user_name=user.username,
547 workspace_id, user_name=user.username,
548 user_email=user.email, message=message, use_rebase=use_rebase)
548 user_email=user.email, message=message, use_rebase=use_rebase)
549 return merge_state
549 return merge_state
550
550
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 pull_request.updated_on = datetime.datetime.now()
553 pull_request.updated_on = datetime.datetime.now()
554
554
555 CommentsModel().create(
555 CommentsModel().create(
556 text=unicode(_('Pull request merged and closed')),
556 text=unicode(_('Pull request merged and closed')),
557 repo=pull_request.target_repo.repo_id,
557 repo=pull_request.target_repo.repo_id,
558 user=user.user_id,
558 user=user.user_id,
559 pull_request=pull_request.pull_request_id,
559 pull_request=pull_request.pull_request_id,
560 f_path=None,
560 f_path=None,
561 line_no=None,
561 line_no=None,
562 closing_pr=True
562 closing_pr=True
563 )
563 )
564
564
565 Session().add(pull_request)
565 Session().add(pull_request)
566 Session().flush()
566 Session().flush()
567 # TODO: paris: replace invalidation with less radical solution
567 # TODO: paris: replace invalidation with less radical solution
568 ScmModel().mark_for_invalidation(
568 ScmModel().mark_for_invalidation(
569 pull_request.target_repo.repo_name)
569 pull_request.target_repo.repo_name)
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
571
571
572 def has_valid_update_type(self, pull_request):
572 def has_valid_update_type(self, pull_request):
573 source_ref_type = pull_request.source_ref_parts.type
573 source_ref_type = pull_request.source_ref_parts.type
574 return source_ref_type in ['book', 'branch', 'tag']
574 return source_ref_type in ['book', 'branch', 'tag']
575
575
576 def update_commits(self, pull_request):
576 def update_commits(self, pull_request):
577 """
577 """
578 Get the updated list of commits for the pull request
578 Get the updated list of commits for the pull request
579 and return the new pull request version and the list
579 and return the new pull request version and the list
580 of commits processed by this update action
580 of commits processed by this update action
581 """
581 """
582 pull_request = self.__get_pull_request(pull_request)
582 pull_request = self.__get_pull_request(pull_request)
583 source_ref_type = pull_request.source_ref_parts.type
583 source_ref_type = pull_request.source_ref_parts.type
584 source_ref_name = pull_request.source_ref_parts.name
584 source_ref_name = pull_request.source_ref_parts.name
585 source_ref_id = pull_request.source_ref_parts.commit_id
585 source_ref_id = pull_request.source_ref_parts.commit_id
586
586
587 if not self.has_valid_update_type(pull_request):
587 if not self.has_valid_update_type(pull_request):
588 log.debug(
588 log.debug(
589 "Skipping update of pull request %s due to ref type: %s",
589 "Skipping update of pull request %s due to ref type: %s",
590 pull_request, source_ref_type)
590 pull_request, source_ref_type)
591 return UpdateResponse(
591 return UpdateResponse(
592 executed=False,
592 executed=False,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
594 old=pull_request, new=None, changes=None)
594 old=pull_request, new=None, changes=None)
595
595
596 source_repo = pull_request.source_repo.scm_instance()
596 source_repo = pull_request.source_repo.scm_instance()
597 try:
597 try:
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
599 except CommitDoesNotExistError:
599 except CommitDoesNotExistError:
600 return UpdateResponse(
600 return UpdateResponse(
601 executed=False,
601 executed=False,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
603 old=pull_request, new=None, changes=None)
603 old=pull_request, new=None, changes=None)
604
604
605 if source_ref_id == source_commit.raw_id:
605 if source_ref_id == source_commit.raw_id:
606 log.debug("Nothing changed in pull request %s", pull_request)
606 log.debug("Nothing changed in pull request %s", pull_request)
607 return UpdateResponse(
607 return UpdateResponse(
608 executed=False,
608 executed=False,
609 reason=UpdateFailureReason.NO_CHANGE,
609 reason=UpdateFailureReason.NO_CHANGE,
610 old=pull_request, new=None, changes=None)
610 old=pull_request, new=None, changes=None)
611
611
612 # Finally there is a need for an update
612 # Finally there is a need for an update
613 pull_request_version = self._create_version_from_snapshot(pull_request)
613 pull_request_version = self._create_version_from_snapshot(pull_request)
614 self._link_comments_to_version(pull_request_version)
614 self._link_comments_to_version(pull_request_version)
615
615
616 target_ref_type = pull_request.target_ref_parts.type
616 target_ref_type = pull_request.target_ref_parts.type
617 target_ref_name = pull_request.target_ref_parts.name
617 target_ref_name = pull_request.target_ref_parts.name
618 target_ref_id = pull_request.target_ref_parts.commit_id
618 target_ref_id = pull_request.target_ref_parts.commit_id
619 target_repo = pull_request.target_repo.scm_instance()
619 target_repo = pull_request.target_repo.scm_instance()
620
620
621 try:
621 try:
622 if target_ref_type in ('tag', 'branch', 'book'):
622 if target_ref_type in ('tag', 'branch', 'book'):
623 target_commit = target_repo.get_commit(target_ref_name)
623 target_commit = target_repo.get_commit(target_ref_name)
624 else:
624 else:
625 target_commit = target_repo.get_commit(target_ref_id)
625 target_commit = target_repo.get_commit(target_ref_id)
626 except CommitDoesNotExistError:
626 except CommitDoesNotExistError:
627 return UpdateResponse(
627 return UpdateResponse(
628 executed=False,
628 executed=False,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
630 old=pull_request, new=None, changes=None)
630 old=pull_request, new=None, changes=None)
631
631
632 # re-compute commit ids
632 # re-compute commit ids
633 old_commit_ids = set(pull_request.revisions)
633 old_commit_ids = set(pull_request.revisions)
634 pre_load = ["author", "branch", "date", "message"]
634 pre_load = ["author", "branch", "date", "message"]
635 commit_ranges = target_repo.compare(
635 commit_ranges = target_repo.compare(
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
637 pre_load=pre_load)
637 pre_load=pre_load)
638
638
639 ancestor = target_repo.get_common_ancestor(
639 ancestor = target_repo.get_common_ancestor(
640 target_commit.raw_id, source_commit.raw_id, source_repo)
640 target_commit.raw_id, source_commit.raw_id, source_repo)
641
641
642 pull_request.source_ref = '%s:%s:%s' % (
642 pull_request.source_ref = '%s:%s:%s' % (
643 source_ref_type, source_ref_name, source_commit.raw_id)
643 source_ref_type, source_ref_name, source_commit.raw_id)
644 pull_request.target_ref = '%s:%s:%s' % (
644 pull_request.target_ref = '%s:%s:%s' % (
645 target_ref_type, target_ref_name, ancestor)
645 target_ref_type, target_ref_name, ancestor)
646 pull_request.revisions = [
646 pull_request.revisions = [
647 commit.raw_id for commit in reversed(commit_ranges)]
647 commit.raw_id for commit in reversed(commit_ranges)]
648 pull_request.updated_on = datetime.datetime.now()
648 pull_request.updated_on = datetime.datetime.now()
649 Session().add(pull_request)
649 Session().add(pull_request)
650 new_commit_ids = set(pull_request.revisions)
650 new_commit_ids = set(pull_request.revisions)
651
651
652 changes = self._calculate_commit_id_changes(
652 changes = self._calculate_commit_id_changes(
653 old_commit_ids, new_commit_ids)
653 old_commit_ids, new_commit_ids)
654
654
655 old_diff_data, new_diff_data = self._generate_update_diffs(
655 old_diff_data, new_diff_data = self._generate_update_diffs(
656 pull_request, pull_request_version)
656 pull_request, pull_request_version)
657
657
658 CommentsModel().outdate_comments(
658 CommentsModel().outdate_comments(
659 pull_request, old_diff_data=old_diff_data,
659 pull_request, old_diff_data=old_diff_data,
660 new_diff_data=new_diff_data)
660 new_diff_data=new_diff_data)
661
661
662 file_changes = self._calculate_file_changes(
662 file_changes = self._calculate_file_changes(
663 old_diff_data, new_diff_data)
663 old_diff_data, new_diff_data)
664
664
665 # Add an automatic comment to the pull request
665 # Add an automatic comment to the pull request
666 update_comment = CommentsModel().create(
666 update_comment = CommentsModel().create(
667 text=self._render_update_message(changes, file_changes),
667 text=self._render_update_message(changes, file_changes),
668 repo=pull_request.target_repo,
668 repo=pull_request.target_repo,
669 user=pull_request.author,
669 user=pull_request.author,
670 pull_request=pull_request,
670 pull_request=pull_request,
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
672
672
673 # Update status to "Under Review" for added commits
673 # Update status to "Under Review" for added commits
674 for commit_id in changes.added:
674 for commit_id in changes.added:
675 ChangesetStatusModel().set_status(
675 ChangesetStatusModel().set_status(
676 repo=pull_request.source_repo,
676 repo=pull_request.source_repo,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
678 comment=update_comment,
678 comment=update_comment,
679 user=pull_request.author,
679 user=pull_request.author,
680 pull_request=pull_request,
680 pull_request=pull_request,
681 revision=commit_id)
681 revision=commit_id)
682
682
683 log.debug(
683 log.debug(
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
685 'removed_ids: %s', pull_request.pull_request_id,
685 'removed_ids: %s', pull_request.pull_request_id,
686 changes.added, changes.common, changes.removed)
686 changes.added, changes.common, changes.removed)
687 log.debug('Updated pull request with the following file changes: %s',
687 log.debug('Updated pull request with the following file changes: %s',
688 file_changes)
688 file_changes)
689
689
690 log.info(
690 log.info(
691 "Updated pull request %s from commit %s to commit %s, "
691 "Updated pull request %s from commit %s to commit %s, "
692 "stored new version %s of this pull request.",
692 "stored new version %s of this pull request.",
693 pull_request.pull_request_id, source_ref_id,
693 pull_request.pull_request_id, source_ref_id,
694 pull_request.source_ref_parts.commit_id,
694 pull_request.source_ref_parts.commit_id,
695 pull_request_version.pull_request_version_id)
695 pull_request_version.pull_request_version_id)
696 Session().commit()
696 Session().commit()
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
698 'update')
698 'update')
699
699
700 return UpdateResponse(
700 return UpdateResponse(
701 executed=True, reason=UpdateFailureReason.NONE,
701 executed=True, reason=UpdateFailureReason.NONE,
702 old=pull_request, new=pull_request_version, changes=changes)
702 old=pull_request, new=pull_request_version, changes=changes)
703
703
704 def _create_version_from_snapshot(self, pull_request):
704 def _create_version_from_snapshot(self, pull_request):
705 version = PullRequestVersion()
705 version = PullRequestVersion()
706 version.title = pull_request.title
706 version.title = pull_request.title
707 version.description = pull_request.description
707 version.description = pull_request.description
708 version.status = pull_request.status
708 version.status = pull_request.status
709 version.created_on = datetime.datetime.now()
709 version.created_on = datetime.datetime.now()
710 version.updated_on = pull_request.updated_on
710 version.updated_on = pull_request.updated_on
711 version.user_id = pull_request.user_id
711 version.user_id = pull_request.user_id
712 version.source_repo = pull_request.source_repo
712 version.source_repo = pull_request.source_repo
713 version.source_ref = pull_request.source_ref
713 version.source_ref = pull_request.source_ref
714 version.target_repo = pull_request.target_repo
714 version.target_repo = pull_request.target_repo
715 version.target_ref = pull_request.target_ref
715 version.target_ref = pull_request.target_ref
716
716
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
719 version._last_merge_status = pull_request._last_merge_status
719 version._last_merge_status = pull_request._last_merge_status
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
721 version.merge_rev = pull_request.merge_rev
721 version.merge_rev = pull_request.merge_rev
722
722
723 version.revisions = pull_request.revisions
723 version.revisions = pull_request.revisions
724 version.pull_request = pull_request
724 version.pull_request = pull_request
725 Session().add(version)
725 Session().add(version)
726 Session().flush()
726 Session().flush()
727
727
728 return version
728 return version
729
729
730 def _generate_update_diffs(self, pull_request, pull_request_version):
730 def _generate_update_diffs(self, pull_request, pull_request_version):
731 diff_context = (
731 diff_context = (
732 self.DIFF_CONTEXT +
732 self.DIFF_CONTEXT +
733 CommentsModel.needed_extra_diff_context())
733 CommentsModel.needed_extra_diff_context())
734 old_diff = self._get_diff_from_pr_or_version(
734 old_diff = self._get_diff_from_pr_or_version(
735 pull_request_version, context=diff_context)
735 pull_request_version, context=diff_context)
736 new_diff = self._get_diff_from_pr_or_version(
736 new_diff = self._get_diff_from_pr_or_version(
737 pull_request, context=diff_context)
737 pull_request, context=diff_context)
738
738
739 old_diff_data = diffs.DiffProcessor(old_diff)
739 old_diff_data = diffs.DiffProcessor(old_diff)
740 old_diff_data.prepare()
740 old_diff_data.prepare()
741 new_diff_data = diffs.DiffProcessor(new_diff)
741 new_diff_data = diffs.DiffProcessor(new_diff)
742 new_diff_data.prepare()
742 new_diff_data.prepare()
743
743
744 return old_diff_data, new_diff_data
744 return old_diff_data, new_diff_data
745
745
746 def _link_comments_to_version(self, pull_request_version):
746 def _link_comments_to_version(self, pull_request_version):
747 """
747 """
748 Link all unlinked comments of this pull request to the given version.
748 Link all unlinked comments of this pull request to the given version.
749
749
750 :param pull_request_version: The `PullRequestVersion` to which
750 :param pull_request_version: The `PullRequestVersion` to which
751 the comments shall be linked.
751 the comments shall be linked.
752
752
753 """
753 """
754 pull_request = pull_request_version.pull_request
754 pull_request = pull_request_version.pull_request
755 comments = ChangesetComment.query().filter(
755 comments = ChangesetComment.query().filter(
756 # TODO: johbo: Should we query for the repo at all here?
756 # TODO: johbo: Should we query for the repo at all here?
757 # Pending decision on how comments of PRs are to be related
757 # Pending decision on how comments of PRs are to be related
758 # to either the source repo, the target repo or no repo at all.
758 # to either the source repo, the target repo or no repo at all.
759 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
759 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
760 ChangesetComment.pull_request == pull_request,
760 ChangesetComment.pull_request == pull_request,
761 ChangesetComment.pull_request_version == None)
761 ChangesetComment.pull_request_version == None)
762
762
763 # TODO: johbo: Find out why this breaks if it is done in a bulk
763 # TODO: johbo: Find out why this breaks if it is done in a bulk
764 # operation.
764 # operation.
765 for comment in comments:
765 for comment in comments:
766 comment.pull_request_version_id = (
766 comment.pull_request_version_id = (
767 pull_request_version.pull_request_version_id)
767 pull_request_version.pull_request_version_id)
768 Session().add(comment)
768 Session().add(comment)
769
769
770 def _calculate_commit_id_changes(self, old_ids, new_ids):
770 def _calculate_commit_id_changes(self, old_ids, new_ids):
771 added = new_ids.difference(old_ids)
771 added = new_ids.difference(old_ids)
772 common = old_ids.intersection(new_ids)
772 common = old_ids.intersection(new_ids)
773 removed = old_ids.difference(new_ids)
773 removed = old_ids.difference(new_ids)
774 return ChangeTuple(added, common, removed)
774 return ChangeTuple(added, common, removed)
775
775
776 def _calculate_file_changes(self, old_diff_data, new_diff_data):
776 def _calculate_file_changes(self, old_diff_data, new_diff_data):
777
777
778 old_files = OrderedDict()
778 old_files = OrderedDict()
779 for diff_data in old_diff_data.parsed_diff:
779 for diff_data in old_diff_data.parsed_diff:
780 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
780 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
781
781
782 added_files = []
782 added_files = []
783 modified_files = []
783 modified_files = []
784 removed_files = []
784 removed_files = []
785 for diff_data in new_diff_data.parsed_diff:
785 for diff_data in new_diff_data.parsed_diff:
786 new_filename = diff_data['filename']
786 new_filename = diff_data['filename']
787 new_hash = md5_safe(diff_data['raw_diff'])
787 new_hash = md5_safe(diff_data['raw_diff'])
788
788
789 old_hash = old_files.get(new_filename)
789 old_hash = old_files.get(new_filename)
790 if not old_hash:
790 if not old_hash:
791 # file is not present in old diff, means it's added
791 # file is not present in old diff, means it's added
792 added_files.append(new_filename)
792 added_files.append(new_filename)
793 else:
793 else:
794 if new_hash != old_hash:
794 if new_hash != old_hash:
795 modified_files.append(new_filename)
795 modified_files.append(new_filename)
796 # now remove a file from old, since we have seen it already
796 # now remove a file from old, since we have seen it already
797 del old_files[new_filename]
797 del old_files[new_filename]
798
798
799 # removed files is when there are present in old, but not in NEW,
799 # removed files is when there are present in old, but not in NEW,
800 # since we remove old files that are present in new diff, left-overs
800 # since we remove old files that are present in new diff, left-overs
801 # if any should be the removed files
801 # if any should be the removed files
802 removed_files.extend(old_files.keys())
802 removed_files.extend(old_files.keys())
803
803
804 return FileChangeTuple(added_files, modified_files, removed_files)
804 return FileChangeTuple(added_files, modified_files, removed_files)
805
805
806 def _render_update_message(self, changes, file_changes):
806 def _render_update_message(self, changes, file_changes):
807 """
807 """
808 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
808 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
809 so it's always looking the same disregarding on which default
809 so it's always looking the same disregarding on which default
810 renderer system is using.
810 renderer system is using.
811
811
812 :param changes: changes named tuple
812 :param changes: changes named tuple
813 :param file_changes: file changes named tuple
813 :param file_changes: file changes named tuple
814
814
815 """
815 """
816 new_status = ChangesetStatus.get_status_lbl(
816 new_status = ChangesetStatus.get_status_lbl(
817 ChangesetStatus.STATUS_UNDER_REVIEW)
817 ChangesetStatus.STATUS_UNDER_REVIEW)
818
818
819 changed_files = (
819 changed_files = (
820 file_changes.added + file_changes.modified + file_changes.removed)
820 file_changes.added + file_changes.modified + file_changes.removed)
821
821
822 params = {
822 params = {
823 'under_review_label': new_status,
823 'under_review_label': new_status,
824 'added_commits': changes.added,
824 'added_commits': changes.added,
825 'removed_commits': changes.removed,
825 'removed_commits': changes.removed,
826 'changed_files': changed_files,
826 'changed_files': changed_files,
827 'added_files': file_changes.added,
827 'added_files': file_changes.added,
828 'modified_files': file_changes.modified,
828 'modified_files': file_changes.modified,
829 'removed_files': file_changes.removed,
829 'removed_files': file_changes.removed,
830 }
830 }
831 renderer = RstTemplateRenderer()
831 renderer = RstTemplateRenderer()
832 return renderer.render('pull_request_update.mako', **params)
832 return renderer.render('pull_request_update.mako', **params)
833
833
834 def edit(self, pull_request, title, description):
834 def edit(self, pull_request, title, description):
835 pull_request = self.__get_pull_request(pull_request)
835 pull_request = self.__get_pull_request(pull_request)
836 if pull_request.is_closed():
836 if pull_request.is_closed():
837 raise ValueError('This pull request is closed')
837 raise ValueError('This pull request is closed')
838 if title:
838 if title:
839 pull_request.title = title
839 pull_request.title = title
840 pull_request.description = description
840 pull_request.description = description
841 pull_request.updated_on = datetime.datetime.now()
841 pull_request.updated_on = datetime.datetime.now()
842 Session().add(pull_request)
842 Session().add(pull_request)
843
843
844 def update_reviewers(self, pull_request, reviewer_data):
844 def update_reviewers(self, pull_request, reviewer_data):
845 """
845 """
846 Update the reviewers in the pull request
846 Update the reviewers in the pull request
847
847
848 :param pull_request: the pr to update
848 :param pull_request: the pr to update
849 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
849 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
850 """
850 """
851
851
852 reviewers_reasons = {}
852 reviewers_reasons = {}
853 for user_id, reasons in reviewer_data:
853 for user_id, reasons in reviewer_data:
854 if isinstance(user_id, (int, basestring)):
854 if isinstance(user_id, (int, basestring)):
855 user_id = self._get_user(user_id).user_id
855 user_id = self._get_user(user_id).user_id
856 reviewers_reasons[user_id] = reasons
856 reviewers_reasons[user_id] = reasons
857
857
858 reviewers_ids = set(reviewers_reasons.keys())
858 reviewers_ids = set(reviewers_reasons.keys())
859 pull_request = self.__get_pull_request(pull_request)
859 pull_request = self.__get_pull_request(pull_request)
860 current_reviewers = PullRequestReviewers.query()\
860 current_reviewers = PullRequestReviewers.query()\
861 .filter(PullRequestReviewers.pull_request ==
861 .filter(PullRequestReviewers.pull_request ==
862 pull_request).all()
862 pull_request).all()
863 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
863 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
864
864
865 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
865 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
866 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
866 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
867
867
868 log.debug("Adding %s reviewers", ids_to_add)
868 log.debug("Adding %s reviewers", ids_to_add)
869 log.debug("Removing %s reviewers", ids_to_remove)
869 log.debug("Removing %s reviewers", ids_to_remove)
870 changed = False
870 changed = False
871 for uid in ids_to_add:
871 for uid in ids_to_add:
872 changed = True
872 changed = True
873 _usr = self._get_user(uid)
873 _usr = self._get_user(uid)
874 reasons = reviewers_reasons[uid]
874 reasons = reviewers_reasons[uid]
875 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
875 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
876 Session().add(reviewer)
876 Session().add(reviewer)
877
877
878 self.notify_reviewers(pull_request, ids_to_add)
878 self.notify_reviewers(pull_request, ids_to_add)
879
879
880 for uid in ids_to_remove:
880 for uid in ids_to_remove:
881 changed = True
881 changed = True
882 reviewer = PullRequestReviewers.query()\
882 reviewer = PullRequestReviewers.query()\
883 .filter(PullRequestReviewers.user_id == uid,
883 .filter(PullRequestReviewers.user_id == uid,
884 PullRequestReviewers.pull_request == pull_request)\
884 PullRequestReviewers.pull_request == pull_request)\
885 .scalar()
885 .scalar()
886 if reviewer:
886 if reviewer:
887 Session().delete(reviewer)
887 Session().delete(reviewer)
888 if changed:
888 if changed:
889 pull_request.updated_on = datetime.datetime.now()
889 pull_request.updated_on = datetime.datetime.now()
890 Session().add(pull_request)
890 Session().add(pull_request)
891
891
892 return ids_to_add, ids_to_remove
892 return ids_to_add, ids_to_remove
893
893
894 def get_url(self, pull_request):
894 def get_url(self, pull_request):
895 return h.url('pullrequest_show',
895 return h.url('pullrequest_show',
896 repo_name=safe_str(pull_request.target_repo.repo_name),
896 repo_name=safe_str(pull_request.target_repo.repo_name),
897 pull_request_id=pull_request.pull_request_id,
897 pull_request_id=pull_request.pull_request_id,
898 qualified=True)
898 qualified=True)
899
899
900 def get_shadow_clone_url(self, pull_request):
900 def get_shadow_clone_url(self, pull_request):
901 """
901 """
902 Returns qualified url pointing to the shadow repository. If this pull
902 Returns qualified url pointing to the shadow repository. If this pull
903 request is closed there is no shadow repository and ``None`` will be
903 request is closed there is no shadow repository and ``None`` will be
904 returned.
904 returned.
905 """
905 """
906 if pull_request.is_closed():
906 if pull_request.is_closed():
907 return None
907 return None
908 else:
908 else:
909 pr_url = urllib.unquote(self.get_url(pull_request))
909 pr_url = urllib.unquote(self.get_url(pull_request))
910 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
910 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
911
911
912 def notify_reviewers(self, pull_request, reviewers_ids):
912 def notify_reviewers(self, pull_request, reviewers_ids):
913 # notification to reviewers
913 # notification to reviewers
914 if not reviewers_ids:
914 if not reviewers_ids:
915 return
915 return
916
916
917 pull_request_obj = pull_request
917 pull_request_obj = pull_request
918 # get the current participants of this pull request
918 # get the current participants of this pull request
919 recipients = reviewers_ids
919 recipients = reviewers_ids
920 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
920 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
921
921
922 pr_source_repo = pull_request_obj.source_repo
922 pr_source_repo = pull_request_obj.source_repo
923 pr_target_repo = pull_request_obj.target_repo
923 pr_target_repo = pull_request_obj.target_repo
924
924
925 pr_url = h.url(
925 pr_url = h.url(
926 'pullrequest_show',
926 'pullrequest_show',
927 repo_name=pr_target_repo.repo_name,
927 repo_name=pr_target_repo.repo_name,
928 pull_request_id=pull_request_obj.pull_request_id,
928 pull_request_id=pull_request_obj.pull_request_id,
929 qualified=True,)
929 qualified=True,)
930
930
931 # set some variables for email notification
931 # set some variables for email notification
932 pr_target_repo_url = h.url(
932 pr_target_repo_url = h.url(
933 'summary_home',
933 'summary_home',
934 repo_name=pr_target_repo.repo_name,
934 repo_name=pr_target_repo.repo_name,
935 qualified=True)
935 qualified=True)
936
936
937 pr_source_repo_url = h.url(
937 pr_source_repo_url = h.url(
938 'summary_home',
938 'summary_home',
939 repo_name=pr_source_repo.repo_name,
939 repo_name=pr_source_repo.repo_name,
940 qualified=True)
940 qualified=True)
941
941
942 # pull request specifics
942 # pull request specifics
943 pull_request_commits = [
943 pull_request_commits = [
944 (x.raw_id, x.message)
944 (x.raw_id, x.message)
945 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
945 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
946
946
947 kwargs = {
947 kwargs = {
948 'user': pull_request.author,
948 'user': pull_request.author,
949 'pull_request': pull_request_obj,
949 'pull_request': pull_request_obj,
950 'pull_request_commits': pull_request_commits,
950 'pull_request_commits': pull_request_commits,
951
951
952 'pull_request_target_repo': pr_target_repo,
952 'pull_request_target_repo': pr_target_repo,
953 'pull_request_target_repo_url': pr_target_repo_url,
953 'pull_request_target_repo_url': pr_target_repo_url,
954
954
955 'pull_request_source_repo': pr_source_repo,
955 'pull_request_source_repo': pr_source_repo,
956 'pull_request_source_repo_url': pr_source_repo_url,
956 'pull_request_source_repo_url': pr_source_repo_url,
957
957
958 'pull_request_url': pr_url,
958 'pull_request_url': pr_url,
959 }
959 }
960
960
961 # pre-generate the subject for notification itself
961 # pre-generate the subject for notification itself
962 (subject,
962 (subject,
963 _h, _e, # we don't care about those
963 _h, _e, # we don't care about those
964 body_plaintext) = EmailNotificationModel().render_email(
964 body_plaintext) = EmailNotificationModel().render_email(
965 notification_type, **kwargs)
965 notification_type, **kwargs)
966
966
967 # create notification objects, and emails
967 # create notification objects, and emails
968 NotificationModel().create(
968 NotificationModel().create(
969 created_by=pull_request.author,
969 created_by=pull_request.author,
970 notification_subject=subject,
970 notification_subject=subject,
971 notification_body=body_plaintext,
971 notification_body=body_plaintext,
972 notification_type=notification_type,
972 notification_type=notification_type,
973 recipients=recipients,
973 recipients=recipients,
974 email_kwargs=kwargs,
974 email_kwargs=kwargs,
975 )
975 )
976
976
977 def delete(self, pull_request):
977 def delete(self, pull_request):
978 pull_request = self.__get_pull_request(pull_request)
978 pull_request = self.__get_pull_request(pull_request)
979 self._cleanup_merge_workspace(pull_request)
979 self._cleanup_merge_workspace(pull_request)
980 Session().delete(pull_request)
980 Session().delete(pull_request)
981
981
982 def close_pull_request(self, pull_request, user):
982 def close_pull_request(self, pull_request, user):
983 pull_request = self.__get_pull_request(pull_request)
983 pull_request = self.__get_pull_request(pull_request)
984 self._cleanup_merge_workspace(pull_request)
984 self._cleanup_merge_workspace(pull_request)
985 pull_request.status = PullRequest.STATUS_CLOSED
985 pull_request.status = PullRequest.STATUS_CLOSED
986 pull_request.updated_on = datetime.datetime.now()
986 pull_request.updated_on = datetime.datetime.now()
987 Session().add(pull_request)
987 Session().add(pull_request)
988 self._trigger_pull_request_hook(
988 self._trigger_pull_request_hook(
989 pull_request, pull_request.author, 'close')
989 pull_request, pull_request.author, 'close')
990 self._log_action('user_closed_pull_request', user, pull_request)
990 self._log_action('user_closed_pull_request', user, pull_request)
991
991
992 def close_pull_request_with_comment(self, pull_request, user, repo,
992 def close_pull_request_with_comment(self, pull_request, user, repo,
993 message=None):
993 message=None):
994 status = ChangesetStatus.STATUS_REJECTED
994 status = ChangesetStatus.STATUS_REJECTED
995
995
996 if not message:
996 if not message:
997 message = (
997 message = (
998 _('Status change %(transition_icon)s %(status)s') % {
998 _('Status change %(transition_icon)s %(status)s') % {
999 'transition_icon': '>',
999 'transition_icon': '>',
1000 'status': ChangesetStatus.get_status_lbl(status)})
1000 'status': ChangesetStatus.get_status_lbl(status)})
1001
1001
1002 internal_message = _('Closing with') + ' ' + message
1002 internal_message = _('Closing with') + ' ' + message
1003
1003
1004 comm = CommentsModel().create(
1004 comm = CommentsModel().create(
1005 text=internal_message,
1005 text=internal_message,
1006 repo=repo.repo_id,
1006 repo=repo.repo_id,
1007 user=user.user_id,
1007 user=user.user_id,
1008 pull_request=pull_request.pull_request_id,
1008 pull_request=pull_request.pull_request_id,
1009 f_path=None,
1009 f_path=None,
1010 line_no=None,
1010 line_no=None,
1011 status_change=ChangesetStatus.get_status_lbl(status),
1011 status_change=ChangesetStatus.get_status_lbl(status),
1012 status_change_type=status,
1012 status_change_type=status,
1013 closing_pr=True
1013 closing_pr=True
1014 )
1014 )
1015
1015
1016 ChangesetStatusModel().set_status(
1016 ChangesetStatusModel().set_status(
1017 repo.repo_id,
1017 repo.repo_id,
1018 status,
1018 status,
1019 user.user_id,
1019 user.user_id,
1020 comm,
1020 comm,
1021 pull_request=pull_request.pull_request_id
1021 pull_request=pull_request.pull_request_id
1022 )
1022 )
1023 Session().flush()
1023 Session().flush()
1024
1024
1025 PullRequestModel().close_pull_request(
1025 PullRequestModel().close_pull_request(
1026 pull_request.pull_request_id, user)
1026 pull_request.pull_request_id, user)
1027
1027
1028 def merge_status(self, pull_request):
1028 def merge_status(self, pull_request):
1029 if not self._is_merge_enabled(pull_request):
1029 if not self._is_merge_enabled(pull_request):
1030 return False, _('Server-side pull request merging is disabled.')
1030 return False, _('Server-side pull request merging is disabled.')
1031 if pull_request.is_closed():
1031 if pull_request.is_closed():
1032 return False, _('This pull request is closed.')
1032 return False, _('This pull request is closed.')
1033 merge_possible, msg = self._check_repo_requirements(
1033 merge_possible, msg = self._check_repo_requirements(
1034 target=pull_request.target_repo, source=pull_request.source_repo)
1034 target=pull_request.target_repo, source=pull_request.source_repo)
1035 if not merge_possible:
1035 if not merge_possible:
1036 return merge_possible, msg
1036 return merge_possible, msg
1037
1037
1038 try:
1038 try:
1039 resp = self._try_merge(pull_request)
1039 resp = self._try_merge(pull_request)
1040 log.debug("Merge response: %s", resp)
1040 log.debug("Merge response: %s", resp)
1041 status = resp.possible, self.merge_status_message(
1041 status = resp.possible, self.merge_status_message(
1042 resp.failure_reason)
1042 resp.failure_reason)
1043 except NotImplementedError:
1043 except NotImplementedError:
1044 status = False, _('Pull request merging is not supported.')
1044 status = False, _('Pull request merging is not supported.')
1045
1045
1046 return status
1046 return status
1047
1047
1048 def _check_repo_requirements(self, target, source):
1048 def _check_repo_requirements(self, target, source):
1049 """
1049 """
1050 Check if `target` and `source` have compatible requirements.
1050 Check if `target` and `source` have compatible requirements.
1051
1051
1052 Currently this is just checking for largefiles.
1052 Currently this is just checking for largefiles.
1053 """
1053 """
1054 target_has_largefiles = self._has_largefiles(target)
1054 target_has_largefiles = self._has_largefiles(target)
1055 source_has_largefiles = self._has_largefiles(source)
1055 source_has_largefiles = self._has_largefiles(source)
1056 merge_possible = True
1056 merge_possible = True
1057 message = u''
1057 message = u''
1058
1058
1059 if target_has_largefiles != source_has_largefiles:
1059 if target_has_largefiles != source_has_largefiles:
1060 merge_possible = False
1060 merge_possible = False
1061 if source_has_largefiles:
1061 if source_has_largefiles:
1062 message = _(
1062 message = _(
1063 'Target repository large files support is disabled.')
1063 'Target repository large files support is disabled.')
1064 else:
1064 else:
1065 message = _(
1065 message = _(
1066 'Source repository large files support is disabled.')
1066 'Source repository large files support is disabled.')
1067
1067
1068 return merge_possible, message
1068 return merge_possible, message
1069
1069
1070 def _has_largefiles(self, repo):
1070 def _has_largefiles(self, repo):
1071 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1071 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1072 'extensions', 'largefiles')
1072 'extensions', 'largefiles')
1073 return largefiles_ui and largefiles_ui[0].active
1073 return largefiles_ui and largefiles_ui[0].active
1074
1074
1075 def _try_merge(self, pull_request):
1075 def _try_merge(self, pull_request):
1076 """
1076 """
1077 Try to merge the pull request and return the merge status.
1077 Try to merge the pull request and return the merge status.
1078 """
1078 """
1079 log.debug(
1079 log.debug(
1080 "Trying out if the pull request %s can be merged.",
1080 "Trying out if the pull request %s can be merged.",
1081 pull_request.pull_request_id)
1081 pull_request.pull_request_id)
1082 target_vcs = pull_request.target_repo.scm_instance()
1082 target_vcs = pull_request.target_repo.scm_instance()
1083
1083
1084 # Refresh the target reference.
1084 # Refresh the target reference.
1085 try:
1085 try:
1086 target_ref = self._refresh_reference(
1086 target_ref = self._refresh_reference(
1087 pull_request.target_ref_parts, target_vcs)
1087 pull_request.target_ref_parts, target_vcs)
1088 except CommitDoesNotExistError:
1088 except CommitDoesNotExistError:
1089 merge_state = MergeResponse(
1089 merge_state = MergeResponse(
1090 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1090 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1091 return merge_state
1091 return merge_state
1092
1092
1093 target_locked = pull_request.target_repo.locked
1093 target_locked = pull_request.target_repo.locked
1094 if target_locked and target_locked[0]:
1094 if target_locked and target_locked[0]:
1095 log.debug("The target repository is locked.")
1095 log.debug("The target repository is locked.")
1096 merge_state = MergeResponse(
1096 merge_state = MergeResponse(
1097 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1097 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1098 elif self._needs_merge_state_refresh(pull_request, target_ref):
1098 elif self._needs_merge_state_refresh(pull_request, target_ref):
1099 log.debug("Refreshing the merge status of the repository.")
1099 log.debug("Refreshing the merge status of the repository.")
1100 merge_state = self._refresh_merge_state(
1100 merge_state = self._refresh_merge_state(
1101 pull_request, target_vcs, target_ref)
1101 pull_request, target_vcs, target_ref)
1102 else:
1102 else:
1103 possible = pull_request.\
1103 possible = pull_request.\
1104 _last_merge_status == MergeFailureReason.NONE
1104 _last_merge_status == MergeFailureReason.NONE
1105 merge_state = MergeResponse(
1105 merge_state = MergeResponse(
1106 possible, False, None, pull_request._last_merge_status)
1106 possible, False, None, pull_request._last_merge_status)
1107
1107
1108 return merge_state
1108 return merge_state
1109
1109
1110 def _refresh_reference(self, reference, vcs_repository):
1110 def _refresh_reference(self, reference, vcs_repository):
1111 if reference.type in ('branch', 'book'):
1111 if reference.type in ('branch', 'book'):
1112 name_or_id = reference.name
1112 name_or_id = reference.name
1113 else:
1113 else:
1114 name_or_id = reference.commit_id
1114 name_or_id = reference.commit_id
1115 refreshed_commit = vcs_repository.get_commit(name_or_id)
1115 refreshed_commit = vcs_repository.get_commit(name_or_id)
1116 refreshed_reference = Reference(
1116 refreshed_reference = Reference(
1117 reference.type, reference.name, refreshed_commit.raw_id)
1117 reference.type, reference.name, refreshed_commit.raw_id)
1118 return refreshed_reference
1118 return refreshed_reference
1119
1119
1120 def _needs_merge_state_refresh(self, pull_request, target_reference):
1120 def _needs_merge_state_refresh(self, pull_request, target_reference):
1121 return not(
1121 return not(
1122 pull_request.revisions and
1122 pull_request.revisions and
1123 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1123 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1124 target_reference.commit_id == pull_request._last_merge_target_rev)
1124 target_reference.commit_id == pull_request._last_merge_target_rev)
1125
1125
1126 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1126 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1127 workspace_id = self._workspace_id(pull_request)
1127 workspace_id = self._workspace_id(pull_request)
1128 source_vcs = pull_request.source_repo.scm_instance()
1128 source_vcs = pull_request.source_repo.scm_instance()
1129 use_rebase = self._use_rebase_for_merging(pull_request)
1129 use_rebase = self._use_rebase_for_merging(pull_request)
1130 merge_state = target_vcs.merge(
1130 merge_state = target_vcs.merge(
1131 target_reference, source_vcs, pull_request.source_ref_parts,
1131 target_reference, source_vcs, pull_request.source_ref_parts,
1132 workspace_id, dry_run=True, use_rebase=use_rebase)
1132 workspace_id, dry_run=True, use_rebase=use_rebase)
1133
1133
1134 # Do not store the response if there was an unknown error.
1134 # Do not store the response if there was an unknown error.
1135 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1135 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1136 pull_request._last_merge_source_rev = \
1136 pull_request._last_merge_source_rev = \
1137 pull_request.source_ref_parts.commit_id
1137 pull_request.source_ref_parts.commit_id
1138 pull_request._last_merge_target_rev = target_reference.commit_id
1138 pull_request._last_merge_target_rev = target_reference.commit_id
1139 pull_request._last_merge_status = merge_state.failure_reason
1139 pull_request._last_merge_status = merge_state.failure_reason
1140 pull_request.shadow_merge_ref = merge_state.merge_ref
1140 pull_request.shadow_merge_ref = merge_state.merge_ref
1141 Session().add(pull_request)
1141 Session().add(pull_request)
1142 Session().commit()
1142 Session().commit()
1143
1143
1144 return merge_state
1144 return merge_state
1145
1145
1146 def _workspace_id(self, pull_request):
1146 def _workspace_id(self, pull_request):
1147 workspace_id = 'pr-%s' % pull_request.pull_request_id
1147 workspace_id = 'pr-%s' % pull_request.pull_request_id
1148 return workspace_id
1148 return workspace_id
1149
1149
1150 def merge_status_message(self, status_code):
1150 def merge_status_message(self, status_code):
1151 """
1151 """
1152 Return a human friendly error message for the given merge status code.
1152 Return a human friendly error message for the given merge status code.
1153 """
1153 """
1154 return self.MERGE_STATUS_MESSAGES[status_code]
1154 return self.MERGE_STATUS_MESSAGES[status_code]
1155
1155
1156 def generate_repo_data(self, repo, commit_id=None, branch=None,
1156 def generate_repo_data(self, repo, commit_id=None, branch=None,
1157 bookmark=None):
1157 bookmark=None):
1158 all_refs, selected_ref = \
1158 all_refs, selected_ref = \
1159 self._get_repo_pullrequest_sources(
1159 self._get_repo_pullrequest_sources(
1160 repo.scm_instance(), commit_id=commit_id,
1160 repo.scm_instance(), commit_id=commit_id,
1161 branch=branch, bookmark=bookmark)
1161 branch=branch, bookmark=bookmark)
1162
1162
1163 refs_select2 = []
1163 refs_select2 = []
1164 for element in all_refs:
1164 for element in all_refs:
1165 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1165 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1166 refs_select2.append({'text': element[1], 'children': children})
1166 refs_select2.append({'text': element[1], 'children': children})
1167
1167
1168 return {
1168 return {
1169 'user': {
1169 'user': {
1170 'user_id': repo.user.user_id,
1170 'user_id': repo.user.user_id,
1171 'username': repo.user.username,
1171 'username': repo.user.username,
1172 'firstname': repo.user.firstname,
1172 'firstname': repo.user.firstname,
1173 'lastname': repo.user.lastname,
1173 'lastname': repo.user.lastname,
1174 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1174 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1175 },
1175 },
1176 'description': h.chop_at_smart(repo.description, '\n'),
1176 'description': h.chop_at_smart(repo.description, '\n'),
1177 'refs': {
1177 'refs': {
1178 'all_refs': all_refs,
1178 'all_refs': all_refs,
1179 'selected_ref': selected_ref,
1179 'selected_ref': selected_ref,
1180 'select2_refs': refs_select2
1180 'select2_refs': refs_select2
1181 }
1181 }
1182 }
1182 }
1183
1183
1184 def generate_pullrequest_title(self, source, source_ref, target):
1184 def generate_pullrequest_title(self, source, source_ref, target):
1185 return u'{source}#{at_ref} to {target}'.format(
1185 return u'{source}#{at_ref} to {target}'.format(
1186 source=source,
1186 source=source,
1187 at_ref=source_ref,
1187 at_ref=source_ref,
1188 target=target,
1188 target=target,
1189 )
1189 )
1190
1190
1191 def _cleanup_merge_workspace(self, pull_request):
1191 def _cleanup_merge_workspace(self, pull_request):
1192 # Merging related cleanup
1192 # Merging related cleanup
1193 target_scm = pull_request.target_repo.scm_instance()
1193 target_scm = pull_request.target_repo.scm_instance()
1194 workspace_id = 'pr-%s' % pull_request.pull_request_id
1194 workspace_id = 'pr-%s' % pull_request.pull_request_id
1195
1195
1196 try:
1196 try:
1197 target_scm.cleanup_merge_workspace(workspace_id)
1197 target_scm.cleanup_merge_workspace(workspace_id)
1198 except NotImplementedError:
1198 except NotImplementedError:
1199 pass
1199 pass
1200
1200
1201 def _get_repo_pullrequest_sources(
1201 def _get_repo_pullrequest_sources(
1202 self, repo, commit_id=None, branch=None, bookmark=None):
1202 self, repo, commit_id=None, branch=None, bookmark=None):
1203 """
1203 """
1204 Return a structure with repo's interesting commits, suitable for
1204 Return a structure with repo's interesting commits, suitable for
1205 the selectors in pullrequest controller
1205 the selectors in pullrequest controller
1206
1206
1207 :param commit_id: a commit that must be in the list somehow
1207 :param commit_id: a commit that must be in the list somehow
1208 and selected by default
1208 and selected by default
1209 :param branch: a branch that must be in the list and selected
1209 :param branch: a branch that must be in the list and selected
1210 by default - even if closed
1210 by default - even if closed
1211 :param bookmark: a bookmark that must be in the list and selected
1211 :param bookmark: a bookmark that must be in the list and selected
1212 """
1212 """
1213
1213
1214 commit_id = safe_str(commit_id) if commit_id else None
1214 commit_id = safe_str(commit_id) if commit_id else None
1215 branch = safe_str(branch) if branch else None
1215 branch = safe_str(branch) if branch else None
1216 bookmark = safe_str(bookmark) if bookmark else None
1216 bookmark = safe_str(bookmark) if bookmark else None
1217
1217
1218 selected = None
1218 selected = None
1219
1219
1220 # order matters: first source that has commit_id in it will be selected
1220 # order matters: first source that has commit_id in it will be selected
1221 sources = []
1221 sources = []
1222 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1222 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1223 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1223 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1224
1224
1225 if commit_id:
1225 if commit_id:
1226 ref_commit = (h.short_id(commit_id), commit_id)
1226 ref_commit = (h.short_id(commit_id), commit_id)
1227 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1227 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1228
1228
1229 sources.append(
1229 sources.append(
1230 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1230 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1231 )
1231 )
1232
1232
1233 groups = []
1233 groups = []
1234 for group_key, ref_list, group_name, match in sources:
1234 for group_key, ref_list, group_name, match in sources:
1235 group_refs = []
1235 group_refs = []
1236 for ref_name, ref_id in ref_list:
1236 for ref_name, ref_id in ref_list:
1237 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1237 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1238 group_refs.append((ref_key, ref_name))
1238 group_refs.append((ref_key, ref_name))
1239
1239
1240 if not selected:
1240 if not selected:
1241 if set([commit_id, match]) & set([ref_id, ref_name]):
1241 if set([commit_id, match]) & set([ref_id, ref_name]):
1242 selected = ref_key
1242 selected = ref_key
1243
1243
1244 if group_refs:
1244 if group_refs:
1245 groups.append((group_refs, group_name))
1245 groups.append((group_refs, group_name))
1246
1246
1247 if not selected:
1247 if not selected:
1248 ref = commit_id or branch or bookmark
1248 ref = commit_id or branch or bookmark
1249 if ref:
1249 if ref:
1250 raise CommitDoesNotExistError(
1250 raise CommitDoesNotExistError(
1251 'No commit refs could be found matching: %s' % ref)
1251 'No commit refs could be found matching: %s' % ref)
1252 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1252 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1253 selected = 'branch:%s:%s' % (
1253 selected = 'branch:%s:%s' % (
1254 repo.DEFAULT_BRANCH_NAME,
1254 repo.DEFAULT_BRANCH_NAME,
1255 repo.branches[repo.DEFAULT_BRANCH_NAME]
1255 repo.branches[repo.DEFAULT_BRANCH_NAME]
1256 )
1256 )
1257 elif repo.commit_ids:
1257 elif repo.commit_ids:
1258 rev = repo.commit_ids[0]
1258 rev = repo.commit_ids[0]
1259 selected = 'rev:%s:%s' % (rev, rev)
1259 selected = 'rev:%s:%s' % (rev, rev)
1260 else:
1260 else:
1261 raise EmptyRepositoryError()
1261 raise EmptyRepositoryError()
1262 return groups, selected
1262 return groups, selected
1263
1263
1264 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1264 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1265 pull_request = self.__get_pull_request(pull_request)
1265 pull_request = self.__get_pull_request(pull_request)
1266 return self._get_diff_from_pr_or_version(pull_request, context=context)
1266 return self._get_diff_from_pr_or_version(pull_request, context=context)
1267
1267
1268 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1268 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1269 source_repo = pr_or_version.source_repo
1269 source_repo = pr_or_version.source_repo
1270
1270
1271 # we swap org/other ref since we run a simple diff on one repo
1271 # we swap org/other ref since we run a simple diff on one repo
1272 target_ref_id = pr_or_version.target_ref_parts.commit_id
1272 target_ref_id = pr_or_version.target_ref_parts.commit_id
1273 source_ref_id = pr_or_version.source_ref_parts.commit_id
1273 source_ref_id = pr_or_version.source_ref_parts.commit_id
1274 target_commit = source_repo.get_commit(
1274 target_commit = source_repo.get_commit(
1275 commit_id=safe_str(target_ref_id))
1275 commit_id=safe_str(target_ref_id))
1276 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1276 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1277 vcs_repo = source_repo.scm_instance()
1277 vcs_repo = source_repo.scm_instance()
1278
1278
1279 # TODO: johbo: In the context of an update, we cannot reach
1279 # TODO: johbo: In the context of an update, we cannot reach
1280 # the old commit anymore with our normal mechanisms. It needs
1280 # the old commit anymore with our normal mechanisms. It needs
1281 # some sort of special support in the vcs layer to avoid this
1281 # some sort of special support in the vcs layer to avoid this
1282 # workaround.
1282 # workaround.
1283 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1283 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1284 vcs_repo.alias == 'git'):
1284 vcs_repo.alias == 'git'):
1285 source_commit.raw_id = safe_str(source_ref_id)
1285 source_commit.raw_id = safe_str(source_ref_id)
1286
1286
1287 log.debug('calculating diff between '
1287 log.debug('calculating diff between '
1288 'source_ref:%s and target_ref:%s for repo `%s`',
1288 'source_ref:%s and target_ref:%s for repo `%s`',
1289 target_ref_id, source_ref_id,
1289 target_ref_id, source_ref_id,
1290 safe_unicode(vcs_repo.path))
1290 safe_unicode(vcs_repo.path))
1291
1291
1292 vcs_diff = vcs_repo.get_diff(
1292 vcs_diff = vcs_repo.get_diff(
1293 commit1=target_commit, commit2=source_commit, context=context)
1293 commit1=target_commit, commit2=source_commit, context=context)
1294 return vcs_diff
1294 return vcs_diff
1295
1295
1296 def _is_merge_enabled(self, pull_request):
1296 def _is_merge_enabled(self, pull_request):
1297 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1297 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1298 settings = settings_model.get_general_settings()
1298 settings = settings_model.get_general_settings()
1299 return settings.get('rhodecode_pr_merge_enabled', False)
1299 return settings.get('rhodecode_pr_merge_enabled', False)
1300
1300
1301 def _use_rebase_for_merging(self, pull_request):
1301 def _use_rebase_for_merging(self, pull_request):
1302 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1302 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1303 settings = settings_model.get_general_settings()
1303 settings = settings_model.get_general_settings()
1304 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1304 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1305
1305
1306 def _log_action(self, action, user, pull_request):
1306 def _log_action(self, action, user, pull_request):
1307 action_logger(
1307 action_logger(
1308 user,
1308 user,
1309 '{action}:{pr_id}'.format(
1309 '{action}:{pr_id}'.format(
1310 action=action, pr_id=pull_request.pull_request_id),
1310 action=action, pr_id=pull_request.pull_request_id),
1311 pull_request.target_repo)
1311 pull_request.target_repo)
1312
1312
1313
1313
1314 class MergeCheck(object):
1315 """
1316 Perform Merge Checks and returns a check object which stores information
1317 about merge errors, and merge conditions
1318 """
1319
1320 def __init__(self):
1321 self.merge_possible = None
1322 self.merge_msg = ''
1323 self.failed = None
1324 self.errors = []
1325
1326 def push_error(self, error_type, message):
1327 self.failed = True
1328 self.errors.append([error_type, message])
1329
1330 @classmethod
1331 def validate(cls, pull_request, user, fail_early=False, translator=None):
1332 # if migrated to pyramid...
1333 # _ = lambda: translator or _ # use passed in translator if any
1334
1335 merge_check = cls()
1336
1337 # permissions
1338 user_allowed_to_merge = PullRequestModel().check_user_merge(
1339 pull_request, user)
1340 if not user_allowed_to_merge:
1341 log.debug("MergeCheck: cannot merge, approval is pending.")
1342
1343 msg = _('User `{}` not allowed to perform merge').format(user)
1344 merge_check.push_error('error', msg)
1345 if fail_early:
1346 return merge_check
1347
1348 # review status
1349 review_status = pull_request.calculated_review_status()
1350 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1351 if not status_approved:
1352 log.debug("MergeCheck: cannot merge, approval is pending.")
1353
1354 msg = _('Pull request reviewer approval is pending.')
1355
1356 merge_check.push_error('warning', msg)
1357
1358 if fail_early:
1359 return merge_check
1360
1361 # left over TODOs
1362 todos = CommentsModel().get_unresolved_todos(pull_request)
1363 if todos:
1364 log.debug("MergeCheck: cannot merge, {} "
1365 "unresolved todos left.".format(len(todos)))
1366
1367 if len(todos) == 1:
1368 msg = _('Cannot merge, {} TODO still not resolved.').format(
1369 len(todos))
1370 else:
1371 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1372 len(todos))
1373
1374 merge_check.push_error('warning', msg)
1375
1376 if fail_early:
1377 return merge_check
1378
1379 # merge possible
1380 merge_status, msg = PullRequestModel().merge_status(pull_request)
1381 merge_check.merge_possible = merge_status
1382 merge_check.merge_msg = msg
1383 if not merge_status:
1384 log.debug(
1385 "MergeCheck: cannot merge, pull request merge not possible.")
1386 merge_check.push_error('warning', msg)
1387
1388 if fail_early:
1389 return merge_check
1390
1391 return merge_check
1392
1393
1314 ChangeTuple = namedtuple('ChangeTuple',
1394 ChangeTuple = namedtuple('ChangeTuple',
1315 ['added', 'common', 'removed'])
1395 ['added', 'common', 'removed'])
1316
1396
1317 FileChangeTuple = namedtuple('FileChangeTuple',
1397 FileChangeTuple = namedtuple('FileChangeTuple',
1318 ['added', 'modified', 'removed'])
1398 ['added', 'modified', 'removed'])
@@ -1,2257 +1,2261 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29
29
30 //--- BASE ------------------//
30 //--- BASE ------------------//
31 .noscript-error {
31 .noscript-error {
32 top: 0;
32 top: 0;
33 left: 0;
33 left: 0;
34 width: 100%;
34 width: 100%;
35 z-index: 101;
35 z-index: 101;
36 text-align: center;
36 text-align: center;
37 font-family: @text-semibold;
37 font-family: @text-semibold;
38 font-size: 120%;
38 font-size: 120%;
39 color: white;
39 color: white;
40 background-color: @alert2;
40 background-color: @alert2;
41 padding: 5px 0 5px 0;
41 padding: 5px 0 5px 0;
42 }
42 }
43
43
44 html {
44 html {
45 display: table;
45 display: table;
46 height: 100%;
46 height: 100%;
47 width: 100%;
47 width: 100%;
48 }
48 }
49
49
50 body {
50 body {
51 display: table-cell;
51 display: table-cell;
52 width: 100%;
52 width: 100%;
53 }
53 }
54
54
55 //--- LAYOUT ------------------//
55 //--- LAYOUT ------------------//
56
56
57 .hidden{
57 .hidden{
58 display: none !important;
58 display: none !important;
59 }
59 }
60
60
61 .box{
61 .box{
62 float: left;
62 float: left;
63 width: 100%;
63 width: 100%;
64 }
64 }
65
65
66 .browser-header {
66 .browser-header {
67 clear: both;
67 clear: both;
68 }
68 }
69 .main {
69 .main {
70 clear: both;
70 clear: both;
71 padding:0 0 @pagepadding;
71 padding:0 0 @pagepadding;
72 height: auto;
72 height: auto;
73
73
74 &:after { //clearfix
74 &:after { //clearfix
75 content:"";
75 content:"";
76 clear:both;
76 clear:both;
77 width:100%;
77 width:100%;
78 display:block;
78 display:block;
79 }
79 }
80 }
80 }
81
81
82 .action-link{
82 .action-link{
83 margin-left: @padding;
83 margin-left: @padding;
84 padding-left: @padding;
84 padding-left: @padding;
85 border-left: @border-thickness solid @border-default-color;
85 border-left: @border-thickness solid @border-default-color;
86 }
86 }
87
87
88 input + .action-link, .action-link.first{
88 input + .action-link, .action-link.first{
89 border-left: none;
89 border-left: none;
90 }
90 }
91
91
92 .action-link.last{
92 .action-link.last{
93 margin-right: @padding;
93 margin-right: @padding;
94 padding-right: @padding;
94 padding-right: @padding;
95 }
95 }
96
96
97 .action-link.active,
97 .action-link.active,
98 .action-link.active a{
98 .action-link.active a{
99 color: @grey4;
99 color: @grey4;
100 }
100 }
101
101
102 ul.simple-list{
102 ul.simple-list{
103 list-style: none;
103 list-style: none;
104 margin: 0;
104 margin: 0;
105 padding: 0;
105 padding: 0;
106 }
106 }
107
107
108 .main-content {
108 .main-content {
109 padding-bottom: @pagepadding;
109 padding-bottom: @pagepadding;
110 }
110 }
111
111
112 .wide-mode-wrapper {
112 .wide-mode-wrapper {
113 max-width:4000px !important;
113 max-width:4000px !important;
114 }
114 }
115
115
116 .wrapper {
116 .wrapper {
117 position: relative;
117 position: relative;
118 max-width: @wrapper-maxwidth;
118 max-width: @wrapper-maxwidth;
119 margin: 0 auto;
119 margin: 0 auto;
120 }
120 }
121
121
122 #content {
122 #content {
123 clear: both;
123 clear: both;
124 padding: 0 @contentpadding;
124 padding: 0 @contentpadding;
125 }
125 }
126
126
127 .advanced-settings-fields{
127 .advanced-settings-fields{
128 input{
128 input{
129 margin-left: @textmargin;
129 margin-left: @textmargin;
130 margin-right: @padding/2;
130 margin-right: @padding/2;
131 }
131 }
132 }
132 }
133
133
134 .cs_files_title {
134 .cs_files_title {
135 margin: @pagepadding 0 0;
135 margin: @pagepadding 0 0;
136 }
136 }
137
137
138 input.inline[type="file"] {
138 input.inline[type="file"] {
139 display: inline;
139 display: inline;
140 }
140 }
141
141
142 .error_page {
142 .error_page {
143 margin: 10% auto;
143 margin: 10% auto;
144
144
145 h1 {
145 h1 {
146 color: @grey2;
146 color: @grey2;
147 }
147 }
148
148
149 .alert {
149 .alert {
150 margin: @padding 0;
150 margin: @padding 0;
151 }
151 }
152
152
153 .error-branding {
153 .error-branding {
154 font-family: @text-semibold;
154 font-family: @text-semibold;
155 color: @grey4;
155 color: @grey4;
156 }
156 }
157
157
158 .error_message {
158 .error_message {
159 font-family: @text-regular;
159 font-family: @text-regular;
160 }
160 }
161
161
162 .sidebar {
162 .sidebar {
163 min-height: 275px;
163 min-height: 275px;
164 margin: 0;
164 margin: 0;
165 padding: 0 0 @sidebarpadding @sidebarpadding;
165 padding: 0 0 @sidebarpadding @sidebarpadding;
166 border: none;
166 border: none;
167 }
167 }
168
168
169 .main-content {
169 .main-content {
170 position: relative;
170 position: relative;
171 margin: 0 @sidebarpadding @sidebarpadding;
171 margin: 0 @sidebarpadding @sidebarpadding;
172 padding: 0 0 0 @sidebarpadding;
172 padding: 0 0 0 @sidebarpadding;
173 border-left: @border-thickness solid @grey5;
173 border-left: @border-thickness solid @grey5;
174
174
175 @media (max-width:767px) {
175 @media (max-width:767px) {
176 clear: both;
176 clear: both;
177 width: 100%;
177 width: 100%;
178 margin: 0;
178 margin: 0;
179 border: none;
179 border: none;
180 }
180 }
181 }
181 }
182
182
183 .inner-column {
183 .inner-column {
184 float: left;
184 float: left;
185 width: 29.75%;
185 width: 29.75%;
186 min-height: 150px;
186 min-height: 150px;
187 margin: @sidebarpadding 2% 0 0;
187 margin: @sidebarpadding 2% 0 0;
188 padding: 0 2% 0 0;
188 padding: 0 2% 0 0;
189 border-right: @border-thickness solid @grey5;
189 border-right: @border-thickness solid @grey5;
190
190
191 @media (max-width:767px) {
191 @media (max-width:767px) {
192 clear: both;
192 clear: both;
193 width: 100%;
193 width: 100%;
194 border: none;
194 border: none;
195 }
195 }
196
196
197 ul {
197 ul {
198 padding-left: 1.25em;
198 padding-left: 1.25em;
199 }
199 }
200
200
201 &:last-child {
201 &:last-child {
202 margin: @sidebarpadding 0 0;
202 margin: @sidebarpadding 0 0;
203 border: none;
203 border: none;
204 }
204 }
205
205
206 h4 {
206 h4 {
207 margin: 0 0 @padding;
207 margin: 0 0 @padding;
208 font-family: @text-semibold;
208 font-family: @text-semibold;
209 }
209 }
210 }
210 }
211 }
211 }
212 .error-page-logo {
212 .error-page-logo {
213 width: 130px;
213 width: 130px;
214 height: 160px;
214 height: 160px;
215 }
215 }
216
216
217 // HEADER
217 // HEADER
218 .header {
218 .header {
219
219
220 // TODO: johbo: Fix login pages, so that they work without a min-height
220 // TODO: johbo: Fix login pages, so that they work without a min-height
221 // for the header and then remove the min-height. I chose a smaller value
221 // for the header and then remove the min-height. I chose a smaller value
222 // intentionally here to avoid rendering issues in the main navigation.
222 // intentionally here to avoid rendering issues in the main navigation.
223 min-height: 49px;
223 min-height: 49px;
224
224
225 position: relative;
225 position: relative;
226 vertical-align: bottom;
226 vertical-align: bottom;
227 padding: 0 @header-padding;
227 padding: 0 @header-padding;
228 background-color: @grey2;
228 background-color: @grey2;
229 color: @grey5;
229 color: @grey5;
230
230
231 .title {
231 .title {
232 overflow: visible;
232 overflow: visible;
233 }
233 }
234
234
235 &:before,
235 &:before,
236 &:after {
236 &:after {
237 content: "";
237 content: "";
238 clear: both;
238 clear: both;
239 width: 100%;
239 width: 100%;
240 }
240 }
241
241
242 // TODO: johbo: Avoids breaking "Repositories" chooser
242 // TODO: johbo: Avoids breaking "Repositories" chooser
243 .select2-container .select2-choice .select2-arrow {
243 .select2-container .select2-choice .select2-arrow {
244 display: none;
244 display: none;
245 }
245 }
246 }
246 }
247
247
248 #header-inner {
248 #header-inner {
249 &.title {
249 &.title {
250 margin: 0;
250 margin: 0;
251 }
251 }
252 &:before,
252 &:before,
253 &:after {
253 &:after {
254 content: "";
254 content: "";
255 clear: both;
255 clear: both;
256 }
256 }
257 }
257 }
258
258
259 // Gists
259 // Gists
260 #files_data {
260 #files_data {
261 clear: both; //for firefox
261 clear: both; //for firefox
262 }
262 }
263 #gistid {
263 #gistid {
264 margin-right: @padding;
264 margin-right: @padding;
265 }
265 }
266
266
267 // Global Settings Editor
267 // Global Settings Editor
268 .textarea.editor {
268 .textarea.editor {
269 float: left;
269 float: left;
270 position: relative;
270 position: relative;
271 max-width: @texteditor-width;
271 max-width: @texteditor-width;
272
272
273 select {
273 select {
274 position: absolute;
274 position: absolute;
275 top:10px;
275 top:10px;
276 right:0;
276 right:0;
277 }
277 }
278
278
279 .CodeMirror {
279 .CodeMirror {
280 margin: 0;
280 margin: 0;
281 }
281 }
282
282
283 .help-block {
283 .help-block {
284 margin: 0 0 @padding;
284 margin: 0 0 @padding;
285 padding:.5em;
285 padding:.5em;
286 background-color: @grey6;
286 background-color: @grey6;
287 }
287 }
288 }
288 }
289
289
290 ul.auth_plugins {
290 ul.auth_plugins {
291 margin: @padding 0 @padding @legend-width;
291 margin: @padding 0 @padding @legend-width;
292 padding: 0;
292 padding: 0;
293
293
294 li {
294 li {
295 margin-bottom: @padding;
295 margin-bottom: @padding;
296 line-height: 1em;
296 line-height: 1em;
297 list-style-type: none;
297 list-style-type: none;
298
298
299 .auth_buttons .btn {
299 .auth_buttons .btn {
300 margin-right: @padding;
300 margin-right: @padding;
301 }
301 }
302
302
303 &:before { content: none; }
303 &:before { content: none; }
304 }
304 }
305 }
305 }
306
306
307
307
308 // My Account PR list
308 // My Account PR list
309
309
310 #show_closed {
310 #show_closed {
311 margin: 0 1em 0 0;
311 margin: 0 1em 0 0;
312 }
312 }
313
313
314 .pullrequestlist {
314 .pullrequestlist {
315 .closed {
315 .closed {
316 background-color: @grey6;
316 background-color: @grey6;
317 }
317 }
318 .td-status {
318 .td-status {
319 padding-left: .5em;
319 padding-left: .5em;
320 }
320 }
321 .log-container .truncate {
321 .log-container .truncate {
322 height: 2.75em;
322 height: 2.75em;
323 white-space: pre-line;
323 white-space: pre-line;
324 }
324 }
325 table.rctable .user {
325 table.rctable .user {
326 padding-left: 0;
326 padding-left: 0;
327 }
327 }
328 table.rctable {
328 table.rctable {
329 td.td-description,
329 td.td-description,
330 .rc-user {
330 .rc-user {
331 min-width: auto;
331 min-width: auto;
332 }
332 }
333 }
333 }
334 }
334 }
335
335
336 // Pull Requests
336 // Pull Requests
337
337
338 .pullrequests_section_head {
338 .pullrequests_section_head {
339 display: block;
339 display: block;
340 clear: both;
340 clear: both;
341 margin: @padding 0;
341 margin: @padding 0;
342 font-family: @text-bold;
342 font-family: @text-bold;
343 }
343 }
344
344
345 .pr-origininfo, .pr-targetinfo {
345 .pr-origininfo, .pr-targetinfo {
346 position: relative;
346 position: relative;
347
347
348 .tag {
348 .tag {
349 display: inline-block;
349 display: inline-block;
350 margin: 0 1em .5em 0;
350 margin: 0 1em .5em 0;
351 }
351 }
352
352
353 .clone-url {
353 .clone-url {
354 display: inline-block;
354 display: inline-block;
355 margin: 0 0 .5em 0;
355 margin: 0 0 .5em 0;
356 padding: 0;
356 padding: 0;
357 line-height: 1.2em;
357 line-height: 1.2em;
358 }
358 }
359 }
359 }
360
360
361 .pr-pullinfo {
361 .pr-pullinfo {
362 clear: both;
362 clear: both;
363 margin: .5em 0;
363 margin: .5em 0;
364 }
364 }
365
365
366 #pr-title-input {
366 #pr-title-input {
367 width: 72%;
367 width: 72%;
368 font-size: 1em;
368 font-size: 1em;
369 font-family: @text-bold;
369 font-family: @text-bold;
370 margin: 0;
370 margin: 0;
371 padding: 0 0 0 @padding/4;
371 padding: 0 0 0 @padding/4;
372 line-height: 1.7em;
372 line-height: 1.7em;
373 color: @text-color;
373 color: @text-color;
374 letter-spacing: .02em;
374 letter-spacing: .02em;
375 }
375 }
376
376
377 #pullrequest_title {
377 #pullrequest_title {
378 width: 100%;
378 width: 100%;
379 box-sizing: border-box;
379 box-sizing: border-box;
380 }
380 }
381
381
382 #pr_open_message {
382 #pr_open_message {
383 border: @border-thickness solid #fff;
383 border: @border-thickness solid #fff;
384 border-radius: @border-radius;
384 border-radius: @border-radius;
385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
386 text-align: right;
386 text-align: right;
387 overflow: hidden;
387 overflow: hidden;
388 }
388 }
389
389
390 .pr-submit-button {
390 .pr-submit-button {
391 float: right;
391 float: right;
392 margin: 0 0 0 5px;
392 margin: 0 0 0 5px;
393 }
393 }
394
394
395 .pr-spacing-container {
395 .pr-spacing-container {
396 padding: 20px;
396 padding: 20px;
397 clear: both
397 clear: both
398 }
398 }
399
399
400 #pr-description-input {
400 #pr-description-input {
401 margin-bottom: 0;
401 margin-bottom: 0;
402 }
402 }
403
403
404 .pr-description-label {
404 .pr-description-label {
405 vertical-align: top;
405 vertical-align: top;
406 }
406 }
407
407
408 .perms_section_head {
408 .perms_section_head {
409 min-width: 625px;
409 min-width: 625px;
410
410
411 h2 {
411 h2 {
412 margin-bottom: 0;
412 margin-bottom: 0;
413 }
413 }
414
414
415 .label-checkbox {
415 .label-checkbox {
416 float: left;
416 float: left;
417 }
417 }
418
418
419 &.field {
419 &.field {
420 margin: @space 0 @padding;
420 margin: @space 0 @padding;
421 }
421 }
422
422
423 &:first-child.field {
423 &:first-child.field {
424 margin-top: 0;
424 margin-top: 0;
425
425
426 .label {
426 .label {
427 margin-top: 0;
427 margin-top: 0;
428 padding-top: 0;
428 padding-top: 0;
429 }
429 }
430
430
431 .radios {
431 .radios {
432 padding-top: 0;
432 padding-top: 0;
433 }
433 }
434 }
434 }
435
435
436 .radios {
436 .radios {
437 float: right;
437 float: right;
438 position: relative;
438 position: relative;
439 width: 405px;
439 width: 405px;
440 }
440 }
441 }
441 }
442
442
443 //--- MODULES ------------------//
443 //--- MODULES ------------------//
444
444
445
445
446 // Server Announcement
446 // Server Announcement
447 #server-announcement {
447 #server-announcement {
448 width: 95%;
448 width: 95%;
449 margin: @padding auto;
449 margin: @padding auto;
450 padding: @padding;
450 padding: @padding;
451 border-width: 2px;
451 border-width: 2px;
452 border-style: solid;
452 border-style: solid;
453 .border-radius(2px);
453 .border-radius(2px);
454 font-family: @text-bold;
454 font-family: @text-bold;
455
455
456 &.info { border-color: @alert4; background-color: @alert4-inner; }
456 &.info { border-color: @alert4; background-color: @alert4-inner; }
457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
458 &.error { border-color: @alert2; background-color: @alert2-inner; }
458 &.error { border-color: @alert2; background-color: @alert2-inner; }
459 &.success { border-color: @alert1; background-color: @alert1-inner; }
459 &.success { border-color: @alert1; background-color: @alert1-inner; }
460 &.neutral { border-color: @grey3; background-color: @grey6; }
460 &.neutral { border-color: @grey3; background-color: @grey6; }
461 }
461 }
462
462
463 // Fixed Sidebar Column
463 // Fixed Sidebar Column
464 .sidebar-col-wrapper {
464 .sidebar-col-wrapper {
465 padding-left: @sidebar-all-width;
465 padding-left: @sidebar-all-width;
466
466
467 .sidebar {
467 .sidebar {
468 width: @sidebar-width;
468 width: @sidebar-width;
469 margin-left: -@sidebar-all-width;
469 margin-left: -@sidebar-all-width;
470 }
470 }
471 }
471 }
472
472
473 .sidebar-col-wrapper.scw-small {
473 .sidebar-col-wrapper.scw-small {
474 padding-left: @sidebar-small-all-width;
474 padding-left: @sidebar-small-all-width;
475
475
476 .sidebar {
476 .sidebar {
477 width: @sidebar-small-width;
477 width: @sidebar-small-width;
478 margin-left: -@sidebar-small-all-width;
478 margin-left: -@sidebar-small-all-width;
479 }
479 }
480 }
480 }
481
481
482
482
483 // FOOTER
483 // FOOTER
484 #footer {
484 #footer {
485 padding: 0;
485 padding: 0;
486 text-align: center;
486 text-align: center;
487 vertical-align: middle;
487 vertical-align: middle;
488 color: @grey2;
488 color: @grey2;
489 background-color: @grey6;
489 background-color: @grey6;
490
490
491 p {
491 p {
492 margin: 0;
492 margin: 0;
493 padding: 1em;
493 padding: 1em;
494 line-height: 1em;
494 line-height: 1em;
495 }
495 }
496
496
497 .server-instance { //server instance
497 .server-instance { //server instance
498 display: none;
498 display: none;
499 }
499 }
500
500
501 .title {
501 .title {
502 float: none;
502 float: none;
503 margin: 0 auto;
503 margin: 0 auto;
504 }
504 }
505 }
505 }
506
506
507 button.close {
507 button.close {
508 padding: 0;
508 padding: 0;
509 cursor: pointer;
509 cursor: pointer;
510 background: transparent;
510 background: transparent;
511 border: 0;
511 border: 0;
512 .box-shadow(none);
512 .box-shadow(none);
513 -webkit-appearance: none;
513 -webkit-appearance: none;
514 }
514 }
515
515
516 .close {
516 .close {
517 float: right;
517 float: right;
518 font-size: 21px;
518 font-size: 21px;
519 font-family: @text-bootstrap;
519 font-family: @text-bootstrap;
520 line-height: 1em;
520 line-height: 1em;
521 font-weight: bold;
521 font-weight: bold;
522 color: @grey2;
522 color: @grey2;
523
523
524 &:hover,
524 &:hover,
525 &:focus {
525 &:focus {
526 color: @grey1;
526 color: @grey1;
527 text-decoration: none;
527 text-decoration: none;
528 cursor: pointer;
528 cursor: pointer;
529 }
529 }
530 }
530 }
531
531
532 // GRID
532 // GRID
533 .sorting,
533 .sorting,
534 .sorting_desc,
534 .sorting_desc,
535 .sorting_asc {
535 .sorting_asc {
536 cursor: pointer;
536 cursor: pointer;
537 }
537 }
538 .sorting_desc:after {
538 .sorting_desc:after {
539 content: "\00A0\25B2";
539 content: "\00A0\25B2";
540 font-size: .75em;
540 font-size: .75em;
541 }
541 }
542 .sorting_asc:after {
542 .sorting_asc:after {
543 content: "\00A0\25BC";
543 content: "\00A0\25BC";
544 font-size: .68em;
544 font-size: .68em;
545 }
545 }
546
546
547
547
548 .user_auth_tokens {
548 .user_auth_tokens {
549
549
550 &.truncate {
550 &.truncate {
551 white-space: nowrap;
551 white-space: nowrap;
552 overflow: hidden;
552 overflow: hidden;
553 text-overflow: ellipsis;
553 text-overflow: ellipsis;
554 }
554 }
555
555
556 .fields .field .input {
556 .fields .field .input {
557 margin: 0;
557 margin: 0;
558 }
558 }
559
559
560 input#description {
560 input#description {
561 width: 100px;
561 width: 100px;
562 margin: 0;
562 margin: 0;
563 }
563 }
564
564
565 .drop-menu {
565 .drop-menu {
566 // TODO: johbo: Remove this, should work out of the box when
566 // TODO: johbo: Remove this, should work out of the box when
567 // having multiple inputs inline
567 // having multiple inputs inline
568 margin: 0 0 0 5px;
568 margin: 0 0 0 5px;
569 }
569 }
570 }
570 }
571 #user_list_table {
571 #user_list_table {
572 .closed {
572 .closed {
573 background-color: @grey6;
573 background-color: @grey6;
574 }
574 }
575 }
575 }
576
576
577
577
578 input {
578 input {
579 &.disabled {
579 &.disabled {
580 opacity: .5;
580 opacity: .5;
581 }
581 }
582 }
582 }
583
583
584 // remove extra padding in firefox
584 // remove extra padding in firefox
585 input::-moz-focus-inner { border:0; padding:0 }
585 input::-moz-focus-inner { border:0; padding:0 }
586
586
587 .adjacent input {
587 .adjacent input {
588 margin-bottom: @padding;
588 margin-bottom: @padding;
589 }
589 }
590
590
591 .permissions_boxes {
591 .permissions_boxes {
592 display: block;
592 display: block;
593 }
593 }
594
594
595 //TODO: lisa: this should be in tables
595 //TODO: lisa: this should be in tables
596 .show_more_col {
596 .show_more_col {
597 width: 20px;
597 width: 20px;
598 }
598 }
599
599
600 //FORMS
600 //FORMS
601
601
602 .medium-inline,
602 .medium-inline,
603 input#description.medium-inline {
603 input#description.medium-inline {
604 display: inline;
604 display: inline;
605 width: @medium-inline-input-width;
605 width: @medium-inline-input-width;
606 min-width: 100px;
606 min-width: 100px;
607 }
607 }
608
608
609 select {
609 select {
610 //reset
610 //reset
611 -webkit-appearance: none;
611 -webkit-appearance: none;
612 -moz-appearance: none;
612 -moz-appearance: none;
613
613
614 display: inline-block;
614 display: inline-block;
615 height: 28px;
615 height: 28px;
616 width: auto;
616 width: auto;
617 margin: 0 @padding @padding 0;
617 margin: 0 @padding @padding 0;
618 padding: 0 18px 0 8px;
618 padding: 0 18px 0 8px;
619 line-height:1em;
619 line-height:1em;
620 font-size: @basefontsize;
620 font-size: @basefontsize;
621 border: @border-thickness solid @rcblue;
621 border: @border-thickness solid @rcblue;
622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
623 color: @rcblue;
623 color: @rcblue;
624
624
625 &:after {
625 &:after {
626 content: "\00A0\25BE";
626 content: "\00A0\25BE";
627 }
627 }
628
628
629 &:focus {
629 &:focus {
630 outline: none;
630 outline: none;
631 }
631 }
632 }
632 }
633
633
634 option {
634 option {
635 &:focus {
635 &:focus {
636 outline: none;
636 outline: none;
637 }
637 }
638 }
638 }
639
639
640 input,
640 input,
641 textarea {
641 textarea {
642 padding: @input-padding;
642 padding: @input-padding;
643 border: @input-border-thickness solid @border-highlight-color;
643 border: @input-border-thickness solid @border-highlight-color;
644 .border-radius (@border-radius);
644 .border-radius (@border-radius);
645 font-family: @text-light;
645 font-family: @text-light;
646 font-size: @basefontsize;
646 font-size: @basefontsize;
647
647
648 &.input-sm {
648 &.input-sm {
649 padding: 5px;
649 padding: 5px;
650 }
650 }
651
651
652 &#description {
652 &#description {
653 min-width: @input-description-minwidth;
653 min-width: @input-description-minwidth;
654 min-height: 1em;
654 min-height: 1em;
655 padding: 10px;
655 padding: 10px;
656 }
656 }
657 }
657 }
658
658
659 .field-sm {
659 .field-sm {
660 input,
660 input,
661 textarea {
661 textarea {
662 padding: 5px;
662 padding: 5px;
663 }
663 }
664 }
664 }
665
665
666 textarea {
666 textarea {
667 display: block;
667 display: block;
668 clear: both;
668 clear: both;
669 width: 100%;
669 width: 100%;
670 min-height: 100px;
670 min-height: 100px;
671 margin-bottom: @padding;
671 margin-bottom: @padding;
672 .box-sizing(border-box);
672 .box-sizing(border-box);
673 overflow: auto;
673 overflow: auto;
674 }
674 }
675
675
676 label {
676 label {
677 font-family: @text-light;
677 font-family: @text-light;
678 }
678 }
679
679
680 // GRAVATARS
680 // GRAVATARS
681 // centers gravatar on username to the right
681 // centers gravatar on username to the right
682
682
683 .gravatar {
683 .gravatar {
684 display: inline;
684 display: inline;
685 min-width: 16px;
685 min-width: 16px;
686 min-height: 16px;
686 min-height: 16px;
687 margin: -5px 0;
687 margin: -5px 0;
688 padding: 0;
688 padding: 0;
689 line-height: 1em;
689 line-height: 1em;
690 border: 1px solid @grey4;
690 border: 1px solid @grey4;
691 box-sizing: content-box;
691 box-sizing: content-box;
692
692
693 &.gravatar-large {
693 &.gravatar-large {
694 margin: -0.5em .25em -0.5em 0;
694 margin: -0.5em .25em -0.5em 0;
695 }
695 }
696
696
697 & + .user {
697 & + .user {
698 display: inline;
698 display: inline;
699 margin: 0;
699 margin: 0;
700 padding: 0 0 0 .17em;
700 padding: 0 0 0 .17em;
701 line-height: 1em;
701 line-height: 1em;
702 }
702 }
703 }
703 }
704
704
705 .user-inline-data {
705 .user-inline-data {
706 display: inline-block;
706 display: inline-block;
707 float: left;
707 float: left;
708 padding-left: .5em;
708 padding-left: .5em;
709 line-height: 1.3em;
709 line-height: 1.3em;
710 }
710 }
711
711
712 .rc-user { // gravatar + user wrapper
712 .rc-user { // gravatar + user wrapper
713 float: left;
713 float: left;
714 position: relative;
714 position: relative;
715 min-width: 100px;
715 min-width: 100px;
716 max-width: 200px;
716 max-width: 200px;
717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
718 display: block;
718 display: block;
719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
720
720
721
721
722 .gravatar {
722 .gravatar {
723 display: block;
723 display: block;
724 position: absolute;
724 position: absolute;
725 top: 0;
725 top: 0;
726 left: 0;
726 left: 0;
727 min-width: @gravatar-size;
727 min-width: @gravatar-size;
728 min-height: @gravatar-size;
728 min-height: @gravatar-size;
729 margin: 0;
729 margin: 0;
730 }
730 }
731
731
732 .user {
732 .user {
733 display: block;
733 display: block;
734 max-width: 175px;
734 max-width: 175px;
735 padding-top: 2px;
735 padding-top: 2px;
736 overflow: hidden;
736 overflow: hidden;
737 text-overflow: ellipsis;
737 text-overflow: ellipsis;
738 }
738 }
739 }
739 }
740
740
741 .gist-gravatar,
741 .gist-gravatar,
742 .journal_container {
742 .journal_container {
743 .gravatar-large {
743 .gravatar-large {
744 margin: 0 .5em -10px 0;
744 margin: 0 .5em -10px 0;
745 }
745 }
746 }
746 }
747
747
748
748
749 // ADMIN SETTINGS
749 // ADMIN SETTINGS
750
750
751 // Tag Patterns
751 // Tag Patterns
752 .tag_patterns {
752 .tag_patterns {
753 .tag_input {
753 .tag_input {
754 margin-bottom: @padding;
754 margin-bottom: @padding;
755 }
755 }
756 }
756 }
757
757
758 .locked_input {
758 .locked_input {
759 position: relative;
759 position: relative;
760
760
761 input {
761 input {
762 display: inline;
762 display: inline;
763 margin-top: 3px;
763 margin-top: 3px;
764 }
764 }
765
765
766 br {
766 br {
767 display: none;
767 display: none;
768 }
768 }
769
769
770 .error-message {
770 .error-message {
771 float: left;
771 float: left;
772 width: 100%;
772 width: 100%;
773 }
773 }
774
774
775 .lock_input_button {
775 .lock_input_button {
776 display: inline;
776 display: inline;
777 }
777 }
778
778
779 .help-block {
779 .help-block {
780 clear: both;
780 clear: both;
781 }
781 }
782 }
782 }
783
783
784 // Notifications
784 // Notifications
785
785
786 .notifications_buttons {
786 .notifications_buttons {
787 margin: 0 0 @space 0;
787 margin: 0 0 @space 0;
788 padding: 0;
788 padding: 0;
789
789
790 .btn {
790 .btn {
791 display: inline-block;
791 display: inline-block;
792 }
792 }
793 }
793 }
794
794
795 .notification-list {
795 .notification-list {
796
796
797 div {
797 div {
798 display: inline-block;
798 display: inline-block;
799 vertical-align: middle;
799 vertical-align: middle;
800 }
800 }
801
801
802 .container {
802 .container {
803 display: block;
803 display: block;
804 margin: 0 0 @padding 0;
804 margin: 0 0 @padding 0;
805 }
805 }
806
806
807 .delete-notifications {
807 .delete-notifications {
808 margin-left: @padding;
808 margin-left: @padding;
809 text-align: right;
809 text-align: right;
810 cursor: pointer;
810 cursor: pointer;
811 }
811 }
812
812
813 .read-notifications {
813 .read-notifications {
814 margin-left: @padding/2;
814 margin-left: @padding/2;
815 text-align: right;
815 text-align: right;
816 width: 35px;
816 width: 35px;
817 cursor: pointer;
817 cursor: pointer;
818 }
818 }
819
819
820 .icon-minus-sign {
820 .icon-minus-sign {
821 color: @alert2;
821 color: @alert2;
822 }
822 }
823
823
824 .icon-ok-sign {
824 .icon-ok-sign {
825 color: @alert1;
825 color: @alert1;
826 }
826 }
827 }
827 }
828
828
829 .user_settings {
829 .user_settings {
830 float: left;
830 float: left;
831 clear: both;
831 clear: both;
832 display: block;
832 display: block;
833 width: 100%;
833 width: 100%;
834
834
835 .gravatar_box {
835 .gravatar_box {
836 margin-bottom: @padding;
836 margin-bottom: @padding;
837
837
838 &:after {
838 &:after {
839 content: " ";
839 content: " ";
840 clear: both;
840 clear: both;
841 width: 100%;
841 width: 100%;
842 }
842 }
843 }
843 }
844
844
845 .fields .field {
845 .fields .field {
846 clear: both;
846 clear: both;
847 }
847 }
848 }
848 }
849
849
850 .advanced_settings {
850 .advanced_settings {
851 margin-bottom: @space;
851 margin-bottom: @space;
852
852
853 .help-block {
853 .help-block {
854 margin-left: 0;
854 margin-left: 0;
855 }
855 }
856
856
857 button + .help-block {
857 button + .help-block {
858 margin-top: @padding;
858 margin-top: @padding;
859 }
859 }
860 }
860 }
861
861
862 // admin settings radio buttons and labels
862 // admin settings radio buttons and labels
863 .label-2 {
863 .label-2 {
864 float: left;
864 float: left;
865 width: @label2-width;
865 width: @label2-width;
866
866
867 label {
867 label {
868 color: @grey1;
868 color: @grey1;
869 }
869 }
870 }
870 }
871 .checkboxes {
871 .checkboxes {
872 float: left;
872 float: left;
873 width: @checkboxes-width;
873 width: @checkboxes-width;
874 margin-bottom: @padding;
874 margin-bottom: @padding;
875
875
876 .checkbox {
876 .checkbox {
877 width: 100%;
877 width: 100%;
878
878
879 label {
879 label {
880 margin: 0;
880 margin: 0;
881 padding: 0;
881 padding: 0;
882 }
882 }
883 }
883 }
884
884
885 .checkbox + .checkbox {
885 .checkbox + .checkbox {
886 display: inline-block;
886 display: inline-block;
887 }
887 }
888
888
889 label {
889 label {
890 margin-right: 1em;
890 margin-right: 1em;
891 }
891 }
892 }
892 }
893
893
894 // CHANGELOG
894 // CHANGELOG
895 .container_header {
895 .container_header {
896 float: left;
896 float: left;
897 display: block;
897 display: block;
898 width: 100%;
898 width: 100%;
899 margin: @padding 0 @padding;
899 margin: @padding 0 @padding;
900
900
901 #filter_changelog {
901 #filter_changelog {
902 float: left;
902 float: left;
903 margin-right: @padding;
903 margin-right: @padding;
904 }
904 }
905
905
906 .breadcrumbs_light {
906 .breadcrumbs_light {
907 display: inline-block;
907 display: inline-block;
908 }
908 }
909 }
909 }
910
910
911 .info_box {
911 .info_box {
912 float: right;
912 float: right;
913 }
913 }
914
914
915
915
916 #graph_nodes {
916 #graph_nodes {
917 padding-top: 43px;
917 padding-top: 43px;
918 }
918 }
919
919
920 #graph_content{
920 #graph_content{
921
921
922 // adjust for table headers so that graph renders properly
922 // adjust for table headers so that graph renders properly
923 // #graph_nodes padding - table cell padding
923 // #graph_nodes padding - table cell padding
924 padding-top: (@space - (@basefontsize * 2.4));
924 padding-top: (@space - (@basefontsize * 2.4));
925
925
926 &.graph_full_width {
926 &.graph_full_width {
927 width: 100%;
927 width: 100%;
928 max-width: 100%;
928 max-width: 100%;
929 }
929 }
930 }
930 }
931
931
932 #graph {
932 #graph {
933 .flag_status {
933 .flag_status {
934 margin: 0;
934 margin: 0;
935 }
935 }
936
936
937 .pagination-left {
937 .pagination-left {
938 float: left;
938 float: left;
939 clear: both;
939 clear: both;
940 }
940 }
941
941
942 .log-container {
942 .log-container {
943 max-width: 345px;
943 max-width: 345px;
944
944
945 .message{
945 .message{
946 max-width: 340px;
946 max-width: 340px;
947 }
947 }
948 }
948 }
949
949
950 .graph-col-wrapper {
950 .graph-col-wrapper {
951 padding-left: 110px;
951 padding-left: 110px;
952
952
953 #graph_nodes {
953 #graph_nodes {
954 width: 100px;
954 width: 100px;
955 margin-left: -110px;
955 margin-left: -110px;
956 float: left;
956 float: left;
957 clear: left;
957 clear: left;
958 }
958 }
959 }
959 }
960 }
960 }
961
961
962 #filter_changelog {
962 #filter_changelog {
963 float: left;
963 float: left;
964 }
964 }
965
965
966
966
967 //--- THEME ------------------//
967 //--- THEME ------------------//
968
968
969 #logo {
969 #logo {
970 float: left;
970 float: left;
971 margin: 9px 0 0 0;
971 margin: 9px 0 0 0;
972
972
973 .header {
973 .header {
974 background-color: transparent;
974 background-color: transparent;
975 }
975 }
976
976
977 a {
977 a {
978 display: inline-block;
978 display: inline-block;
979 }
979 }
980
980
981 img {
981 img {
982 height:30px;
982 height:30px;
983 }
983 }
984 }
984 }
985
985
986 .logo-wrapper {
986 .logo-wrapper {
987 float:left;
987 float:left;
988 }
988 }
989
989
990 .branding{
990 .branding{
991 float: left;
991 float: left;
992 padding: 9px 2px;
992 padding: 9px 2px;
993 line-height: 1em;
993 line-height: 1em;
994 font-size: @navigation-fontsize;
994 font-size: @navigation-fontsize;
995 }
995 }
996
996
997 img {
997 img {
998 border: none;
998 border: none;
999 outline: none;
999 outline: none;
1000 }
1000 }
1001 user-profile-header
1001 user-profile-header
1002 label {
1002 label {
1003
1003
1004 input[type="checkbox"] {
1004 input[type="checkbox"] {
1005 margin-right: 1em;
1005 margin-right: 1em;
1006 }
1006 }
1007 input[type="radio"] {
1007 input[type="radio"] {
1008 margin-right: 1em;
1008 margin-right: 1em;
1009 }
1009 }
1010 }
1010 }
1011
1011
1012 .flag_status {
1012 .flag_status {
1013 margin: 2px 8px 6px 2px;
1013 margin: 2px 8px 6px 2px;
1014 &.under_review {
1014 &.under_review {
1015 .circle(5px, @alert3);
1015 .circle(5px, @alert3);
1016 }
1016 }
1017 &.approved {
1017 &.approved {
1018 .circle(5px, @alert1);
1018 .circle(5px, @alert1);
1019 }
1019 }
1020 &.rejected,
1020 &.rejected,
1021 &.forced_closed{
1021 &.forced_closed{
1022 .circle(5px, @alert2);
1022 .circle(5px, @alert2);
1023 }
1023 }
1024 &.not_reviewed {
1024 &.not_reviewed {
1025 .circle(5px, @grey5);
1025 .circle(5px, @grey5);
1026 }
1026 }
1027 }
1027 }
1028
1028
1029 .flag_status_comment_box {
1029 .flag_status_comment_box {
1030 margin: 5px 6px 0px 2px;
1030 margin: 5px 6px 0px 2px;
1031 }
1031 }
1032 .test_pattern_preview {
1032 .test_pattern_preview {
1033 margin: @space 0;
1033 margin: @space 0;
1034
1034
1035 p {
1035 p {
1036 margin-bottom: 0;
1036 margin-bottom: 0;
1037 border-bottom: @border-thickness solid @border-default-color;
1037 border-bottom: @border-thickness solid @border-default-color;
1038 color: @grey3;
1038 color: @grey3;
1039 }
1039 }
1040
1040
1041 .btn {
1041 .btn {
1042 margin-bottom: @padding;
1042 margin-bottom: @padding;
1043 }
1043 }
1044 }
1044 }
1045 #test_pattern_result {
1045 #test_pattern_result {
1046 display: none;
1046 display: none;
1047 &:extend(pre);
1047 &:extend(pre);
1048 padding: .9em;
1048 padding: .9em;
1049 color: @grey3;
1049 color: @grey3;
1050 background-color: @grey7;
1050 background-color: @grey7;
1051 border-right: @border-thickness solid @border-default-color;
1051 border-right: @border-thickness solid @border-default-color;
1052 border-bottom: @border-thickness solid @border-default-color;
1052 border-bottom: @border-thickness solid @border-default-color;
1053 border-left: @border-thickness solid @border-default-color;
1053 border-left: @border-thickness solid @border-default-color;
1054 }
1054 }
1055
1055
1056 #repo_vcs_settings {
1056 #repo_vcs_settings {
1057 #inherit_overlay_vcs_default {
1057 #inherit_overlay_vcs_default {
1058 display: none;
1058 display: none;
1059 }
1059 }
1060 #inherit_overlay_vcs_custom {
1060 #inherit_overlay_vcs_custom {
1061 display: custom;
1061 display: custom;
1062 }
1062 }
1063 &.inherited {
1063 &.inherited {
1064 #inherit_overlay_vcs_default {
1064 #inherit_overlay_vcs_default {
1065 display: block;
1065 display: block;
1066 }
1066 }
1067 #inherit_overlay_vcs_custom {
1067 #inherit_overlay_vcs_custom {
1068 display: none;
1068 display: none;
1069 }
1069 }
1070 }
1070 }
1071 }
1071 }
1072
1072
1073 .issue-tracker-link {
1073 .issue-tracker-link {
1074 color: @rcblue;
1074 color: @rcblue;
1075 }
1075 }
1076
1076
1077 // Issue Tracker Table Show/Hide
1077 // Issue Tracker Table Show/Hide
1078 #repo_issue_tracker {
1078 #repo_issue_tracker {
1079 #inherit_overlay {
1079 #inherit_overlay {
1080 display: none;
1080 display: none;
1081 }
1081 }
1082 #custom_overlay {
1082 #custom_overlay {
1083 display: custom;
1083 display: custom;
1084 }
1084 }
1085 &.inherited {
1085 &.inherited {
1086 #inherit_overlay {
1086 #inherit_overlay {
1087 display: block;
1087 display: block;
1088 }
1088 }
1089 #custom_overlay {
1089 #custom_overlay {
1090 display: none;
1090 display: none;
1091 }
1091 }
1092 }
1092 }
1093 }
1093 }
1094 table.issuetracker {
1094 table.issuetracker {
1095 &.readonly {
1095 &.readonly {
1096 tr, td {
1096 tr, td {
1097 color: @grey3;
1097 color: @grey3;
1098 }
1098 }
1099 }
1099 }
1100 .edit {
1100 .edit {
1101 display: none;
1101 display: none;
1102 }
1102 }
1103 .editopen {
1103 .editopen {
1104 .edit {
1104 .edit {
1105 display: inline;
1105 display: inline;
1106 }
1106 }
1107 .entry {
1107 .entry {
1108 display: none;
1108 display: none;
1109 }
1109 }
1110 }
1110 }
1111 tr td.td-action {
1111 tr td.td-action {
1112 min-width: 117px;
1112 min-width: 117px;
1113 }
1113 }
1114 td input {
1114 td input {
1115 max-width: none;
1115 max-width: none;
1116 min-width: 30px;
1116 min-width: 30px;
1117 width: 80%;
1117 width: 80%;
1118 }
1118 }
1119 .issuetracker_pref input {
1119 .issuetracker_pref input {
1120 width: 40%;
1120 width: 40%;
1121 }
1121 }
1122 input.edit_issuetracker_update {
1122 input.edit_issuetracker_update {
1123 margin-right: 0;
1123 margin-right: 0;
1124 width: auto;
1124 width: auto;
1125 }
1125 }
1126 }
1126 }
1127
1127
1128 table.integrations {
1128 table.integrations {
1129 .td-icon {
1129 .td-icon {
1130 width: 20px;
1130 width: 20px;
1131 .integration-icon {
1131 .integration-icon {
1132 height: 20px;
1132 height: 20px;
1133 width: 20px;
1133 width: 20px;
1134 }
1134 }
1135 }
1135 }
1136 }
1136 }
1137
1137
1138 .integrations {
1138 .integrations {
1139 a.integration-box {
1139 a.integration-box {
1140 color: @text-color;
1140 color: @text-color;
1141 &:hover {
1141 &:hover {
1142 .panel {
1142 .panel {
1143 background: #fbfbfb;
1143 background: #fbfbfb;
1144 }
1144 }
1145 }
1145 }
1146 .integration-icon {
1146 .integration-icon {
1147 width: 30px;
1147 width: 30px;
1148 height: 30px;
1148 height: 30px;
1149 margin-right: 20px;
1149 margin-right: 20px;
1150 float: left;
1150 float: left;
1151 }
1151 }
1152
1152
1153 .panel-body {
1153 .panel-body {
1154 padding: 10px;
1154 padding: 10px;
1155 }
1155 }
1156 .panel {
1156 .panel {
1157 margin-bottom: 10px;
1157 margin-bottom: 10px;
1158 }
1158 }
1159 h2 {
1159 h2 {
1160 display: inline-block;
1160 display: inline-block;
1161 margin: 0;
1161 margin: 0;
1162 min-width: 140px;
1162 min-width: 140px;
1163 }
1163 }
1164 }
1164 }
1165 }
1165 }
1166
1166
1167 //Permissions Settings
1167 //Permissions Settings
1168 #add_perm {
1168 #add_perm {
1169 margin: 0 0 @padding;
1169 margin: 0 0 @padding;
1170 cursor: pointer;
1170 cursor: pointer;
1171 }
1171 }
1172
1172
1173 .perm_ac {
1173 .perm_ac {
1174 input {
1174 input {
1175 width: 95%;
1175 width: 95%;
1176 }
1176 }
1177 }
1177 }
1178
1178
1179 .autocomplete-suggestions {
1179 .autocomplete-suggestions {
1180 width: auto !important; // overrides autocomplete.js
1180 width: auto !important; // overrides autocomplete.js
1181 margin: 0;
1181 margin: 0;
1182 border: @border-thickness solid @rcblue;
1182 border: @border-thickness solid @rcblue;
1183 border-radius: @border-radius;
1183 border-radius: @border-radius;
1184 color: @rcblue;
1184 color: @rcblue;
1185 background-color: white;
1185 background-color: white;
1186 }
1186 }
1187 .autocomplete-selected {
1187 .autocomplete-selected {
1188 background: #F0F0F0;
1188 background: #F0F0F0;
1189 }
1189 }
1190 .ac-container-wrap {
1190 .ac-container-wrap {
1191 margin: 0;
1191 margin: 0;
1192 padding: 8px;
1192 padding: 8px;
1193 border-bottom: @border-thickness solid @rclightblue;
1193 border-bottom: @border-thickness solid @rclightblue;
1194 list-style-type: none;
1194 list-style-type: none;
1195 cursor: pointer;
1195 cursor: pointer;
1196
1196
1197 &:hover {
1197 &:hover {
1198 background-color: @rclightblue;
1198 background-color: @rclightblue;
1199 }
1199 }
1200
1200
1201 img {
1201 img {
1202 height: @gravatar-size;
1202 height: @gravatar-size;
1203 width: @gravatar-size;
1203 width: @gravatar-size;
1204 margin-right: 1em;
1204 margin-right: 1em;
1205 }
1205 }
1206
1206
1207 strong {
1207 strong {
1208 font-weight: normal;
1208 font-weight: normal;
1209 }
1209 }
1210 }
1210 }
1211
1211
1212 // Settings Dropdown
1212 // Settings Dropdown
1213 .user-menu .container {
1213 .user-menu .container {
1214 padding: 0 4px;
1214 padding: 0 4px;
1215 margin: 0;
1215 margin: 0;
1216 }
1216 }
1217
1217
1218 .user-menu .gravatar {
1218 .user-menu .gravatar {
1219 cursor: pointer;
1219 cursor: pointer;
1220 }
1220 }
1221
1221
1222 .codeblock {
1222 .codeblock {
1223 margin-bottom: @padding;
1223 margin-bottom: @padding;
1224 clear: both;
1224 clear: both;
1225
1225
1226 .stats{
1226 .stats{
1227 overflow: hidden;
1227 overflow: hidden;
1228 }
1228 }
1229
1229
1230 .message{
1230 .message{
1231 textarea{
1231 textarea{
1232 margin: 0;
1232 margin: 0;
1233 }
1233 }
1234 }
1234 }
1235
1235
1236 .code-header {
1236 .code-header {
1237 .stats {
1237 .stats {
1238 line-height: 2em;
1238 line-height: 2em;
1239
1239
1240 .revision_id {
1240 .revision_id {
1241 margin-left: 0;
1241 margin-left: 0;
1242 }
1242 }
1243 .buttons {
1243 .buttons {
1244 padding-right: 0;
1244 padding-right: 0;
1245 }
1245 }
1246 }
1246 }
1247
1247
1248 .item{
1248 .item{
1249 margin-right: 0.5em;
1249 margin-right: 0.5em;
1250 }
1250 }
1251 }
1251 }
1252
1252
1253 #editor_container{
1253 #editor_container{
1254 position: relative;
1254 position: relative;
1255 margin: @padding;
1255 margin: @padding;
1256 }
1256 }
1257 }
1257 }
1258
1258
1259 #file_history_container {
1259 #file_history_container {
1260 display: none;
1260 display: none;
1261 }
1261 }
1262
1262
1263 .file-history-inner {
1263 .file-history-inner {
1264 margin-bottom: 10px;
1264 margin-bottom: 10px;
1265 }
1265 }
1266
1266
1267 // Pull Requests
1267 // Pull Requests
1268 .summary-details {
1268 .summary-details {
1269 width: 72%;
1269 width: 72%;
1270 }
1270 }
1271 .pr-summary {
1271 .pr-summary {
1272 border-bottom: @border-thickness solid @grey5;
1272 border-bottom: @border-thickness solid @grey5;
1273 margin-bottom: @space;
1273 margin-bottom: @space;
1274 }
1274 }
1275 .reviewers-title {
1275 .reviewers-title {
1276 width: 25%;
1276 width: 25%;
1277 min-width: 200px;
1277 min-width: 200px;
1278 }
1278 }
1279 .reviewers {
1279 .reviewers {
1280 width: 25%;
1280 width: 25%;
1281 min-width: 200px;
1281 min-width: 200px;
1282 }
1282 }
1283 .reviewers ul li {
1283 .reviewers ul li {
1284 position: relative;
1284 position: relative;
1285 width: 100%;
1285 width: 100%;
1286 margin-bottom: 8px;
1286 margin-bottom: 8px;
1287 }
1287 }
1288 .reviewers_member {
1288 .reviewers_member {
1289 width: 100%;
1289 width: 100%;
1290 overflow: auto;
1290 overflow: auto;
1291 }
1291 }
1292 .reviewer_reason {
1292 .reviewer_reason {
1293 padding-left: 20px;
1293 padding-left: 20px;
1294 }
1294 }
1295 .reviewer_status {
1295 .reviewer_status {
1296 display: inline-block;
1296 display: inline-block;
1297 vertical-align: top;
1297 vertical-align: top;
1298 width: 7%;
1298 width: 7%;
1299 min-width: 20px;
1299 min-width: 20px;
1300 height: 1.2em;
1300 height: 1.2em;
1301 margin-top: 3px;
1301 margin-top: 3px;
1302 line-height: 1em;
1302 line-height: 1em;
1303 }
1303 }
1304
1304
1305 .reviewer_name {
1305 .reviewer_name {
1306 display: inline-block;
1306 display: inline-block;
1307 max-width: 83%;
1307 max-width: 83%;
1308 padding-right: 20px;
1308 padding-right: 20px;
1309 vertical-align: middle;
1309 vertical-align: middle;
1310 line-height: 1;
1310 line-height: 1;
1311
1311
1312 .rc-user {
1312 .rc-user {
1313 min-width: 0;
1313 min-width: 0;
1314 margin: -2px 1em 0 0;
1314 margin: -2px 1em 0 0;
1315 }
1315 }
1316
1316
1317 .reviewer {
1317 .reviewer {
1318 float: left;
1318 float: left;
1319 }
1319 }
1320
1320
1321 &.to-delete {
1321 &.to-delete {
1322 .user,
1322 .user,
1323 .reviewer {
1323 .reviewer {
1324 text-decoration: line-through;
1324 text-decoration: line-through;
1325 }
1325 }
1326 }
1326 }
1327 }
1327 }
1328
1328
1329 .reviewer_member_remove {
1329 .reviewer_member_remove {
1330 position: absolute;
1330 position: absolute;
1331 right: 0;
1331 right: 0;
1332 top: 0;
1332 top: 0;
1333 width: 16px;
1333 width: 16px;
1334 margin-bottom: 10px;
1334 margin-bottom: 10px;
1335 padding: 0;
1335 padding: 0;
1336 color: black;
1336 color: black;
1337 }
1337 }
1338 .reviewer_member_status {
1338 .reviewer_member_status {
1339 margin-top: 5px;
1339 margin-top: 5px;
1340 }
1340 }
1341 .pr-summary #summary{
1341 .pr-summary #summary{
1342 width: 100%;
1342 width: 100%;
1343 }
1343 }
1344 .pr-summary .action_button:hover {
1344 .pr-summary .action_button:hover {
1345 border: 0;
1345 border: 0;
1346 cursor: pointer;
1346 cursor: pointer;
1347 }
1347 }
1348 .pr-details-title {
1348 .pr-details-title {
1349 padding-bottom: 8px;
1349 padding-bottom: 8px;
1350 border-bottom: @border-thickness solid @grey5;
1350 border-bottom: @border-thickness solid @grey5;
1351
1351
1352 .action_button.disabled {
1352 .action_button.disabled {
1353 color: @grey4;
1353 color: @grey4;
1354 cursor: inherit;
1354 cursor: inherit;
1355 }
1355 }
1356 .action_button {
1356 .action_button {
1357 color: @rcblue;
1357 color: @rcblue;
1358 }
1358 }
1359 }
1359 }
1360 .pr-details-content {
1360 .pr-details-content {
1361 margin-top: @textmargin;
1361 margin-top: @textmargin;
1362 margin-bottom: @textmargin;
1362 margin-bottom: @textmargin;
1363 }
1363 }
1364 .pr-description {
1364 .pr-description {
1365 white-space:pre-wrap;
1365 white-space:pre-wrap;
1366 }
1366 }
1367 .group_members {
1367 .group_members {
1368 margin-top: 0;
1368 margin-top: 0;
1369 padding: 0;
1369 padding: 0;
1370 list-style: outside none none;
1370 list-style: outside none none;
1371
1371
1372 img {
1372 img {
1373 height: @gravatar-size;
1373 height: @gravatar-size;
1374 width: @gravatar-size;
1374 width: @gravatar-size;
1375 margin-right: .5em;
1375 margin-right: .5em;
1376 margin-left: 3px;
1376 margin-left: 3px;
1377 }
1377 }
1378
1378
1379 .to-delete {
1379 .to-delete {
1380 .user {
1380 .user {
1381 text-decoration: line-through;
1381 text-decoration: line-through;
1382 }
1382 }
1383 }
1383 }
1384 }
1384 }
1385
1385
1386 .compare_view_commits_title {
1386 .compare_view_commits_title {
1387 .disabled {
1387 .disabled {
1388 cursor: inherit;
1388 cursor: inherit;
1389 &:hover{
1389 &:hover{
1390 background-color: inherit;
1390 background-color: inherit;
1391 color: inherit;
1391 color: inherit;
1392 }
1392 }
1393 }
1393 }
1394 }
1394 }
1395
1395
1396 // new entry in group_members
1396 // new entry in group_members
1397 .td-author-new-entry {
1397 .td-author-new-entry {
1398 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1398 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1399 }
1399 }
1400
1400
1401 .usergroup_member_remove {
1401 .usergroup_member_remove {
1402 width: 16px;
1402 width: 16px;
1403 margin-bottom: 10px;
1403 margin-bottom: 10px;
1404 padding: 0;
1404 padding: 0;
1405 color: black !important;
1405 color: black !important;
1406 cursor: pointer;
1406 cursor: pointer;
1407 }
1407 }
1408
1408
1409 .reviewer_ac .ac-input {
1409 .reviewer_ac .ac-input {
1410 width: 92%;
1410 width: 92%;
1411 margin-bottom: 1em;
1411 margin-bottom: 1em;
1412 }
1412 }
1413
1413
1414 .compare_view_commits tr{
1414 .compare_view_commits tr{
1415 height: 20px;
1415 height: 20px;
1416 }
1416 }
1417 .compare_view_commits td {
1417 .compare_view_commits td {
1418 vertical-align: top;
1418 vertical-align: top;
1419 padding-top: 10px;
1419 padding-top: 10px;
1420 }
1420 }
1421 .compare_view_commits .author {
1421 .compare_view_commits .author {
1422 margin-left: 5px;
1422 margin-left: 5px;
1423 }
1423 }
1424
1424
1425 .compare_view_files {
1425 .compare_view_files {
1426 width: 100%;
1426 width: 100%;
1427
1427
1428 td {
1428 td {
1429 vertical-align: middle;
1429 vertical-align: middle;
1430 }
1430 }
1431 }
1431 }
1432
1432
1433 .compare_view_filepath {
1433 .compare_view_filepath {
1434 color: @grey1;
1434 color: @grey1;
1435 }
1435 }
1436
1436
1437 .show_more {
1437 .show_more {
1438 display: inline-block;
1438 display: inline-block;
1439 position: relative;
1439 position: relative;
1440 vertical-align: middle;
1440 vertical-align: middle;
1441 width: 4px;
1441 width: 4px;
1442 height: @basefontsize;
1442 height: @basefontsize;
1443
1443
1444 &:after {
1444 &:after {
1445 content: "\00A0\25BE";
1445 content: "\00A0\25BE";
1446 display: inline-block;
1446 display: inline-block;
1447 width:10px;
1447 width:10px;
1448 line-height: 5px;
1448 line-height: 5px;
1449 font-size: 12px;
1449 font-size: 12px;
1450 cursor: pointer;
1450 cursor: pointer;
1451 }
1451 }
1452 }
1452 }
1453
1453
1454 .journal_more .show_more {
1454 .journal_more .show_more {
1455 display: inline;
1455 display: inline;
1456
1456
1457 &:after {
1457 &:after {
1458 content: none;
1458 content: none;
1459 }
1459 }
1460 }
1460 }
1461
1461
1462 .open .show_more:after,
1462 .open .show_more:after,
1463 .select2-dropdown-open .show_more:after {
1463 .select2-dropdown-open .show_more:after {
1464 .rotate(180deg);
1464 .rotate(180deg);
1465 margin-left: 4px;
1465 margin-left: 4px;
1466 }
1466 }
1467
1467
1468
1468
1469 .compare_view_commits .collapse_commit:after {
1469 .compare_view_commits .collapse_commit:after {
1470 cursor: pointer;
1470 cursor: pointer;
1471 content: "\00A0\25B4";
1471 content: "\00A0\25B4";
1472 margin-left: -3px;
1472 margin-left: -3px;
1473 font-size: 17px;
1473 font-size: 17px;
1474 color: @grey4;
1474 color: @grey4;
1475 }
1475 }
1476
1476
1477 .diff_links {
1477 .diff_links {
1478 margin-left: 8px;
1478 margin-left: 8px;
1479 }
1479 }
1480
1480
1481 div.ancestor {
1481 div.ancestor {
1482 margin: -30px 0px;
1482 margin: -30px 0px;
1483 }
1483 }
1484
1484
1485 .cs_icon_td input[type="checkbox"] {
1485 .cs_icon_td input[type="checkbox"] {
1486 display: none;
1486 display: none;
1487 }
1487 }
1488
1488
1489 .cs_icon_td .expand_file_icon:after {
1489 .cs_icon_td .expand_file_icon:after {
1490 cursor: pointer;
1490 cursor: pointer;
1491 content: "\00A0\25B6";
1491 content: "\00A0\25B6";
1492 font-size: 12px;
1492 font-size: 12px;
1493 color: @grey4;
1493 color: @grey4;
1494 }
1494 }
1495
1495
1496 .cs_icon_td .collapse_file_icon:after {
1496 .cs_icon_td .collapse_file_icon:after {
1497 cursor: pointer;
1497 cursor: pointer;
1498 content: "\00A0\25BC";
1498 content: "\00A0\25BC";
1499 font-size: 12px;
1499 font-size: 12px;
1500 color: @grey4;
1500 color: @grey4;
1501 }
1501 }
1502
1502
1503 /*new binary
1503 /*new binary
1504 NEW_FILENODE = 1
1504 NEW_FILENODE = 1
1505 DEL_FILENODE = 2
1505 DEL_FILENODE = 2
1506 MOD_FILENODE = 3
1506 MOD_FILENODE = 3
1507 RENAMED_FILENODE = 4
1507 RENAMED_FILENODE = 4
1508 COPIED_FILENODE = 5
1508 COPIED_FILENODE = 5
1509 CHMOD_FILENODE = 6
1509 CHMOD_FILENODE = 6
1510 BIN_FILENODE = 7
1510 BIN_FILENODE = 7
1511 */
1511 */
1512 .cs_files_expand {
1512 .cs_files_expand {
1513 font-size: @basefontsize + 5px;
1513 font-size: @basefontsize + 5px;
1514 line-height: 1.8em;
1514 line-height: 1.8em;
1515 float: right;
1515 float: right;
1516 }
1516 }
1517
1517
1518 .cs_files_expand span{
1518 .cs_files_expand span{
1519 color: @rcblue;
1519 color: @rcblue;
1520 cursor: pointer;
1520 cursor: pointer;
1521 }
1521 }
1522 .cs_files {
1522 .cs_files {
1523 clear: both;
1523 clear: both;
1524 padding-bottom: @padding;
1524 padding-bottom: @padding;
1525
1525
1526 .cur_cs {
1526 .cur_cs {
1527 margin: 10px 2px;
1527 margin: 10px 2px;
1528 font-weight: bold;
1528 font-weight: bold;
1529 }
1529 }
1530
1530
1531 .node {
1531 .node {
1532 float: left;
1532 float: left;
1533 }
1533 }
1534
1534
1535 .changes {
1535 .changes {
1536 float: right;
1536 float: right;
1537 color: white;
1537 color: white;
1538 font-size: @basefontsize - 4px;
1538 font-size: @basefontsize - 4px;
1539 margin-top: 4px;
1539 margin-top: 4px;
1540 opacity: 0.6;
1540 opacity: 0.6;
1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1542
1542
1543 .added {
1543 .added {
1544 background-color: @alert1;
1544 background-color: @alert1;
1545 float: left;
1545 float: left;
1546 text-align: center;
1546 text-align: center;
1547 }
1547 }
1548
1548
1549 .deleted {
1549 .deleted {
1550 background-color: @alert2;
1550 background-color: @alert2;
1551 float: left;
1551 float: left;
1552 text-align: center;
1552 text-align: center;
1553 }
1553 }
1554
1554
1555 .bin {
1555 .bin {
1556 background-color: @alert1;
1556 background-color: @alert1;
1557 text-align: center;
1557 text-align: center;
1558 }
1558 }
1559
1559
1560 /*new binary*/
1560 /*new binary*/
1561 .bin.bin1 {
1561 .bin.bin1 {
1562 background-color: @alert1;
1562 background-color: @alert1;
1563 text-align: center;
1563 text-align: center;
1564 }
1564 }
1565
1565
1566 /*deleted binary*/
1566 /*deleted binary*/
1567 .bin.bin2 {
1567 .bin.bin2 {
1568 background-color: @alert2;
1568 background-color: @alert2;
1569 text-align: center;
1569 text-align: center;
1570 }
1570 }
1571
1571
1572 /*mod binary*/
1572 /*mod binary*/
1573 .bin.bin3 {
1573 .bin.bin3 {
1574 background-color: @grey2;
1574 background-color: @grey2;
1575 text-align: center;
1575 text-align: center;
1576 }
1576 }
1577
1577
1578 /*rename file*/
1578 /*rename file*/
1579 .bin.bin4 {
1579 .bin.bin4 {
1580 background-color: @alert4;
1580 background-color: @alert4;
1581 text-align: center;
1581 text-align: center;
1582 }
1582 }
1583
1583
1584 /*copied file*/
1584 /*copied file*/
1585 .bin.bin5 {
1585 .bin.bin5 {
1586 background-color: @alert4;
1586 background-color: @alert4;
1587 text-align: center;
1587 text-align: center;
1588 }
1588 }
1589
1589
1590 /*chmod file*/
1590 /*chmod file*/
1591 .bin.bin6 {
1591 .bin.bin6 {
1592 background-color: @grey2;
1592 background-color: @grey2;
1593 text-align: center;
1593 text-align: center;
1594 }
1594 }
1595 }
1595 }
1596 }
1596 }
1597
1597
1598 .cs_files .cs_added, .cs_files .cs_A,
1598 .cs_files .cs_added, .cs_files .cs_A,
1599 .cs_files .cs_added, .cs_files .cs_M,
1599 .cs_files .cs_added, .cs_files .cs_M,
1600 .cs_files .cs_added, .cs_files .cs_D {
1600 .cs_files .cs_added, .cs_files .cs_D {
1601 height: 16px;
1601 height: 16px;
1602 padding-right: 10px;
1602 padding-right: 10px;
1603 margin-top: 7px;
1603 margin-top: 7px;
1604 text-align: left;
1604 text-align: left;
1605 }
1605 }
1606
1606
1607 .cs_icon_td {
1607 .cs_icon_td {
1608 min-width: 16px;
1608 min-width: 16px;
1609 width: 16px;
1609 width: 16px;
1610 }
1610 }
1611
1611
1612 .pull-request-merge {
1612 .pull-request-merge {
1613 border: 1px solid @grey5;
1613 border: 1px solid @grey5;
1614 padding: 10px 0px 20px;
1614 padding: 10px 0px 20px;
1615 margin-top: 10px;
1615 margin-top: 10px;
1616 margin-bottom: 20px;
1616 margin-bottom: 20px;
1617 }
1617 }
1618
1618
1619 .pull-request-merge ul {
1619 .pull-request-merge ul {
1620 padding: 0px 0px;
1620 padding: 0px 0px;
1621 }
1621 }
1622
1622
1623 .pull-request-merge li:before{
1623 .pull-request-merge li:before{
1624 content:none;
1624 content:none;
1625 }
1625 }
1626
1626
1627 .pull-request-merge .pull-request-wrap {
1627 .pull-request-merge .pull-request-wrap {
1628 height: auto;
1628 height: auto;
1629 padding: 0px 0px;
1629 padding: 0px 0px;
1630 text-align: right;
1630 text-align: right;
1631 }
1631 }
1632
1632
1633 .pull-request-merge span {
1633 .pull-request-merge span {
1634 margin-right: 5px;
1634 margin-right: 5px;
1635 }
1635 }
1636
1636
1637 .pull-request-merge-actions {
1637 .pull-request-merge-actions {
1638 height: 30px;
1638 height: 30px;
1639 padding: 0px 0px;
1639 padding: 0px 0px;
1640 }
1640 }
1641
1641
1642 .merge-status {
1643 margin-right: 5px;
1644 }
1645
1642 .merge-message {
1646 .merge-message {
1643 font-size: 1.2em
1647 font-size: 1.2em
1644 }
1648 }
1645 .merge-message li{
1646 text-decoration: none;
1647 }
1648
1649
1649 .merge-message.success i {
1650 .merge-message.success i,
1651 .merge-icon.success i {
1650 color:@alert1;
1652 color:@alert1;
1651 }
1653 }
1652 .merge-message.warning i {
1654
1655 .merge-message.warning i,
1656 .merge-icon.warning i {
1653 color: @alert3;
1657 color: @alert3;
1654 }
1658 }
1655 .merge-message.error i {
1659
1660 .merge-message.error i,
1661 .merge-icon.error i {
1656 color:@alert2;
1662 color:@alert2;
1657 }
1663 }
1658
1664
1659
1660
1661 .pr-versions {
1665 .pr-versions {
1662 position: relative;
1666 position: relative;
1663 top: 6px;
1667 top: 6px;
1664 }
1668 }
1665
1669
1666 #close_pull_request {
1670 #close_pull_request {
1667 margin-right: 0px;
1671 margin-right: 0px;
1668 }
1672 }
1669
1673
1670 .empty_data {
1674 .empty_data {
1671 color: @grey4;
1675 color: @grey4;
1672 }
1676 }
1673
1677
1674 #changeset_compare_view_content {
1678 #changeset_compare_view_content {
1675 margin-bottom: @space;
1679 margin-bottom: @space;
1676 clear: both;
1680 clear: both;
1677 width: 100%;
1681 width: 100%;
1678 box-sizing: border-box;
1682 box-sizing: border-box;
1679 .border-radius(@border-radius);
1683 .border-radius(@border-radius);
1680
1684
1681 .help-block {
1685 .help-block {
1682 margin: @padding 0;
1686 margin: @padding 0;
1683 color: @text-color;
1687 color: @text-color;
1684 }
1688 }
1685
1689
1686 .empty_data {
1690 .empty_data {
1687 margin: @padding 0;
1691 margin: @padding 0;
1688 }
1692 }
1689
1693
1690 .alert {
1694 .alert {
1691 margin-bottom: @space;
1695 margin-bottom: @space;
1692 }
1696 }
1693 }
1697 }
1694
1698
1695 .table_disp {
1699 .table_disp {
1696 .status {
1700 .status {
1697 width: auto;
1701 width: auto;
1698
1702
1699 .flag_status {
1703 .flag_status {
1700 float: left;
1704 float: left;
1701 }
1705 }
1702 }
1706 }
1703 }
1707 }
1704
1708
1705 .status_box_menu {
1709 .status_box_menu {
1706 margin: 0;
1710 margin: 0;
1707 }
1711 }
1708
1712
1709 .notification-table{
1713 .notification-table{
1710 margin-bottom: @space;
1714 margin-bottom: @space;
1711 display: table;
1715 display: table;
1712 width: 100%;
1716 width: 100%;
1713
1717
1714 .container{
1718 .container{
1715 display: table-row;
1719 display: table-row;
1716
1720
1717 .notification-header{
1721 .notification-header{
1718 border-bottom: @border-thickness solid @border-default-color;
1722 border-bottom: @border-thickness solid @border-default-color;
1719 }
1723 }
1720
1724
1721 .notification-subject{
1725 .notification-subject{
1722 display: table-cell;
1726 display: table-cell;
1723 }
1727 }
1724 }
1728 }
1725 }
1729 }
1726
1730
1727 // Notifications
1731 // Notifications
1728 .notification-header{
1732 .notification-header{
1729 display: table;
1733 display: table;
1730 width: 100%;
1734 width: 100%;
1731 padding: floor(@basefontsize/2) 0;
1735 padding: floor(@basefontsize/2) 0;
1732 line-height: 1em;
1736 line-height: 1em;
1733
1737
1734 .desc, .delete-notifications, .read-notifications{
1738 .desc, .delete-notifications, .read-notifications{
1735 display: table-cell;
1739 display: table-cell;
1736 text-align: left;
1740 text-align: left;
1737 }
1741 }
1738
1742
1739 .desc{
1743 .desc{
1740 width: 1163px;
1744 width: 1163px;
1741 }
1745 }
1742
1746
1743 .delete-notifications, .read-notifications{
1747 .delete-notifications, .read-notifications{
1744 width: 35px;
1748 width: 35px;
1745 min-width: 35px; //fixes when only one button is displayed
1749 min-width: 35px; //fixes when only one button is displayed
1746 }
1750 }
1747 }
1751 }
1748
1752
1749 .notification-body {
1753 .notification-body {
1750 .markdown-block,
1754 .markdown-block,
1751 .rst-block {
1755 .rst-block {
1752 padding: @padding 0;
1756 padding: @padding 0;
1753 }
1757 }
1754
1758
1755 .notification-subject {
1759 .notification-subject {
1756 padding: @textmargin 0;
1760 padding: @textmargin 0;
1757 border-bottom: @border-thickness solid @border-default-color;
1761 border-bottom: @border-thickness solid @border-default-color;
1758 }
1762 }
1759 }
1763 }
1760
1764
1761
1765
1762 .notifications_buttons{
1766 .notifications_buttons{
1763 float: right;
1767 float: right;
1764 }
1768 }
1765
1769
1766 #notification-status{
1770 #notification-status{
1767 display: inline;
1771 display: inline;
1768 }
1772 }
1769
1773
1770 // Repositories
1774 // Repositories
1771
1775
1772 #summary.fields{
1776 #summary.fields{
1773 display: table;
1777 display: table;
1774
1778
1775 .field{
1779 .field{
1776 display: table-row;
1780 display: table-row;
1777
1781
1778 .label-summary{
1782 .label-summary{
1779 display: table-cell;
1783 display: table-cell;
1780 min-width: @label-summary-minwidth;
1784 min-width: @label-summary-minwidth;
1781 padding-top: @padding/2;
1785 padding-top: @padding/2;
1782 padding-bottom: @padding/2;
1786 padding-bottom: @padding/2;
1783 padding-right: @padding/2;
1787 padding-right: @padding/2;
1784 }
1788 }
1785
1789
1786 .input{
1790 .input{
1787 display: table-cell;
1791 display: table-cell;
1788 padding: @padding/2;
1792 padding: @padding/2;
1789
1793
1790 input{
1794 input{
1791 min-width: 29em;
1795 min-width: 29em;
1792 padding: @padding/4;
1796 padding: @padding/4;
1793 }
1797 }
1794 }
1798 }
1795 .statistics, .downloads{
1799 .statistics, .downloads{
1796 .disabled{
1800 .disabled{
1797 color: @grey4;
1801 color: @grey4;
1798 }
1802 }
1799 }
1803 }
1800 }
1804 }
1801 }
1805 }
1802
1806
1803 #summary{
1807 #summary{
1804 width: 70%;
1808 width: 70%;
1805 }
1809 }
1806
1810
1807
1811
1808 // Journal
1812 // Journal
1809 .journal.title {
1813 .journal.title {
1810 h5 {
1814 h5 {
1811 float: left;
1815 float: left;
1812 margin: 0;
1816 margin: 0;
1813 width: 70%;
1817 width: 70%;
1814 }
1818 }
1815
1819
1816 ul {
1820 ul {
1817 float: right;
1821 float: right;
1818 display: inline-block;
1822 display: inline-block;
1819 margin: 0;
1823 margin: 0;
1820 width: 30%;
1824 width: 30%;
1821 text-align: right;
1825 text-align: right;
1822
1826
1823 li {
1827 li {
1824 display: inline;
1828 display: inline;
1825 font-size: @journal-fontsize;
1829 font-size: @journal-fontsize;
1826 line-height: 1em;
1830 line-height: 1em;
1827
1831
1828 &:before { content: none; }
1832 &:before { content: none; }
1829 }
1833 }
1830 }
1834 }
1831 }
1835 }
1832
1836
1833 .filterexample {
1837 .filterexample {
1834 position: absolute;
1838 position: absolute;
1835 top: 95px;
1839 top: 95px;
1836 left: @contentpadding;
1840 left: @contentpadding;
1837 color: @rcblue;
1841 color: @rcblue;
1838 font-size: 11px;
1842 font-size: 11px;
1839 font-family: @text-regular;
1843 font-family: @text-regular;
1840 cursor: help;
1844 cursor: help;
1841
1845
1842 &:hover {
1846 &:hover {
1843 color: @rcdarkblue;
1847 color: @rcdarkblue;
1844 }
1848 }
1845
1849
1846 @media (max-width:768px) {
1850 @media (max-width:768px) {
1847 position: relative;
1851 position: relative;
1848 top: auto;
1852 top: auto;
1849 left: auto;
1853 left: auto;
1850 display: block;
1854 display: block;
1851 }
1855 }
1852 }
1856 }
1853
1857
1854
1858
1855 #journal{
1859 #journal{
1856 margin-bottom: @space;
1860 margin-bottom: @space;
1857
1861
1858 .journal_day{
1862 .journal_day{
1859 margin-bottom: @textmargin/2;
1863 margin-bottom: @textmargin/2;
1860 padding-bottom: @textmargin/2;
1864 padding-bottom: @textmargin/2;
1861 font-size: @journal-fontsize;
1865 font-size: @journal-fontsize;
1862 border-bottom: @border-thickness solid @border-default-color;
1866 border-bottom: @border-thickness solid @border-default-color;
1863 }
1867 }
1864
1868
1865 .journal_container{
1869 .journal_container{
1866 margin-bottom: @space;
1870 margin-bottom: @space;
1867
1871
1868 .journal_user{
1872 .journal_user{
1869 display: inline-block;
1873 display: inline-block;
1870 }
1874 }
1871 .journal_action_container{
1875 .journal_action_container{
1872 display: block;
1876 display: block;
1873 margin-top: @textmargin;
1877 margin-top: @textmargin;
1874
1878
1875 div{
1879 div{
1876 display: inline;
1880 display: inline;
1877 }
1881 }
1878
1882
1879 div.journal_action_params{
1883 div.journal_action_params{
1880 display: block;
1884 display: block;
1881 }
1885 }
1882
1886
1883 div.journal_repo:after{
1887 div.journal_repo:after{
1884 content: "\A";
1888 content: "\A";
1885 white-space: pre;
1889 white-space: pre;
1886 }
1890 }
1887
1891
1888 div.date{
1892 div.date{
1889 display: block;
1893 display: block;
1890 margin-bottom: @textmargin;
1894 margin-bottom: @textmargin;
1891 }
1895 }
1892 }
1896 }
1893 }
1897 }
1894 }
1898 }
1895
1899
1896 // Files
1900 // Files
1897 .edit-file-title {
1901 .edit-file-title {
1898 border-bottom: @border-thickness solid @border-default-color;
1902 border-bottom: @border-thickness solid @border-default-color;
1899
1903
1900 .breadcrumbs {
1904 .breadcrumbs {
1901 margin-bottom: 0;
1905 margin-bottom: 0;
1902 }
1906 }
1903 }
1907 }
1904
1908
1905 .edit-file-fieldset {
1909 .edit-file-fieldset {
1906 margin-top: @sidebarpadding;
1910 margin-top: @sidebarpadding;
1907
1911
1908 .fieldset {
1912 .fieldset {
1909 .left-label {
1913 .left-label {
1910 width: 13%;
1914 width: 13%;
1911 }
1915 }
1912 .right-content {
1916 .right-content {
1913 width: 87%;
1917 width: 87%;
1914 max-width: 100%;
1918 max-width: 100%;
1915 }
1919 }
1916 .filename-label {
1920 .filename-label {
1917 margin-top: 13px;
1921 margin-top: 13px;
1918 }
1922 }
1919 .commit-message-label {
1923 .commit-message-label {
1920 margin-top: 4px;
1924 margin-top: 4px;
1921 }
1925 }
1922 .file-upload-input {
1926 .file-upload-input {
1923 input {
1927 input {
1924 display: none;
1928 display: none;
1925 }
1929 }
1926 }
1930 }
1927 p {
1931 p {
1928 margin-top: 5px;
1932 margin-top: 5px;
1929 }
1933 }
1930
1934
1931 }
1935 }
1932 .custom-path-link {
1936 .custom-path-link {
1933 margin-left: 5px;
1937 margin-left: 5px;
1934 }
1938 }
1935 #commit {
1939 #commit {
1936 resize: vertical;
1940 resize: vertical;
1937 }
1941 }
1938 }
1942 }
1939
1943
1940 .delete-file-preview {
1944 .delete-file-preview {
1941 max-height: 250px;
1945 max-height: 250px;
1942 }
1946 }
1943
1947
1944 .new-file,
1948 .new-file,
1945 #filter_activate,
1949 #filter_activate,
1946 #filter_deactivate {
1950 #filter_deactivate {
1947 float: left;
1951 float: left;
1948 margin: 0 0 0 15px;
1952 margin: 0 0 0 15px;
1949 }
1953 }
1950
1954
1951 h3.files_location{
1955 h3.files_location{
1952 line-height: 2.4em;
1956 line-height: 2.4em;
1953 }
1957 }
1954
1958
1955 .browser-nav {
1959 .browser-nav {
1956 display: table;
1960 display: table;
1957 margin-bottom: @space;
1961 margin-bottom: @space;
1958
1962
1959
1963
1960 .info_box {
1964 .info_box {
1961 display: inline-table;
1965 display: inline-table;
1962 height: 2.5em;
1966 height: 2.5em;
1963
1967
1964 .browser-cur-rev, .info_box_elem {
1968 .browser-cur-rev, .info_box_elem {
1965 display: table-cell;
1969 display: table-cell;
1966 vertical-align: middle;
1970 vertical-align: middle;
1967 }
1971 }
1968
1972
1969 .info_box_elem {
1973 .info_box_elem {
1970 border-top: @border-thickness solid @rcblue;
1974 border-top: @border-thickness solid @rcblue;
1971 border-bottom: @border-thickness solid @rcblue;
1975 border-bottom: @border-thickness solid @rcblue;
1972
1976
1973 #at_rev, a {
1977 #at_rev, a {
1974 padding: 0.6em 0.9em;
1978 padding: 0.6em 0.9em;
1975 margin: 0;
1979 margin: 0;
1976 .box-shadow(none);
1980 .box-shadow(none);
1977 border: 0;
1981 border: 0;
1978 height: 12px;
1982 height: 12px;
1979 }
1983 }
1980
1984
1981 input#at_rev {
1985 input#at_rev {
1982 max-width: 50px;
1986 max-width: 50px;
1983 text-align: right;
1987 text-align: right;
1984 }
1988 }
1985
1989
1986 &.previous {
1990 &.previous {
1987 border: @border-thickness solid @rcblue;
1991 border: @border-thickness solid @rcblue;
1988 .disabled {
1992 .disabled {
1989 color: @grey4;
1993 color: @grey4;
1990 cursor: not-allowed;
1994 cursor: not-allowed;
1991 }
1995 }
1992 }
1996 }
1993
1997
1994 &.next {
1998 &.next {
1995 border: @border-thickness solid @rcblue;
1999 border: @border-thickness solid @rcblue;
1996 .disabled {
2000 .disabled {
1997 color: @grey4;
2001 color: @grey4;
1998 cursor: not-allowed;
2002 cursor: not-allowed;
1999 }
2003 }
2000 }
2004 }
2001 }
2005 }
2002
2006
2003 .browser-cur-rev {
2007 .browser-cur-rev {
2004
2008
2005 span{
2009 span{
2006 margin: 0;
2010 margin: 0;
2007 color: @rcblue;
2011 color: @rcblue;
2008 height: 12px;
2012 height: 12px;
2009 display: inline-block;
2013 display: inline-block;
2010 padding: 0.7em 1em ;
2014 padding: 0.7em 1em ;
2011 border: @border-thickness solid @rcblue;
2015 border: @border-thickness solid @rcblue;
2012 margin-right: @padding;
2016 margin-right: @padding;
2013 }
2017 }
2014 }
2018 }
2015 }
2019 }
2016
2020
2017 .search_activate {
2021 .search_activate {
2018 display: table-cell;
2022 display: table-cell;
2019 vertical-align: middle;
2023 vertical-align: middle;
2020
2024
2021 input, label{
2025 input, label{
2022 margin: 0;
2026 margin: 0;
2023 padding: 0;
2027 padding: 0;
2024 }
2028 }
2025
2029
2026 input{
2030 input{
2027 margin-left: @textmargin;
2031 margin-left: @textmargin;
2028 }
2032 }
2029
2033
2030 }
2034 }
2031 }
2035 }
2032
2036
2033 .browser-cur-rev{
2037 .browser-cur-rev{
2034 margin-bottom: @textmargin;
2038 margin-bottom: @textmargin;
2035 }
2039 }
2036
2040
2037 #node_filter_box_loading{
2041 #node_filter_box_loading{
2038 .info_text;
2042 .info_text;
2039 }
2043 }
2040
2044
2041 .browser-search {
2045 .browser-search {
2042 margin: -25px 0px 5px 0px;
2046 margin: -25px 0px 5px 0px;
2043 }
2047 }
2044
2048
2045 .node-filter {
2049 .node-filter {
2046 font-size: @repo-title-fontsize;
2050 font-size: @repo-title-fontsize;
2047 padding: 4px 0px 0px 0px;
2051 padding: 4px 0px 0px 0px;
2048
2052
2049 .node-filter-path {
2053 .node-filter-path {
2050 float: left;
2054 float: left;
2051 color: @grey4;
2055 color: @grey4;
2052 }
2056 }
2053 .node-filter-input {
2057 .node-filter-input {
2054 float: left;
2058 float: left;
2055 margin: -2px 0px 0px 2px;
2059 margin: -2px 0px 0px 2px;
2056 input {
2060 input {
2057 padding: 2px;
2061 padding: 2px;
2058 border: none;
2062 border: none;
2059 font-size: @repo-title-fontsize;
2063 font-size: @repo-title-fontsize;
2060 }
2064 }
2061 }
2065 }
2062 }
2066 }
2063
2067
2064
2068
2065 .browser-result{
2069 .browser-result{
2066 td a{
2070 td a{
2067 margin-left: 0.5em;
2071 margin-left: 0.5em;
2068 display: inline-block;
2072 display: inline-block;
2069
2073
2070 em{
2074 em{
2071 font-family: @text-bold;
2075 font-family: @text-bold;
2072 }
2076 }
2073 }
2077 }
2074 }
2078 }
2075
2079
2076 .browser-highlight{
2080 .browser-highlight{
2077 background-color: @grey5-alpha;
2081 background-color: @grey5-alpha;
2078 }
2082 }
2079
2083
2080
2084
2081 // Search
2085 // Search
2082
2086
2083 .search-form{
2087 .search-form{
2084 #q {
2088 #q {
2085 width: @search-form-width;
2089 width: @search-form-width;
2086 }
2090 }
2087 .fields{
2091 .fields{
2088 margin: 0 0 @space;
2092 margin: 0 0 @space;
2089 }
2093 }
2090
2094
2091 label{
2095 label{
2092 display: inline-block;
2096 display: inline-block;
2093 margin-right: @textmargin;
2097 margin-right: @textmargin;
2094 padding-top: 0.25em;
2098 padding-top: 0.25em;
2095 }
2099 }
2096
2100
2097
2101
2098 .results{
2102 .results{
2099 clear: both;
2103 clear: both;
2100 margin: 0 0 @padding;
2104 margin: 0 0 @padding;
2101 }
2105 }
2102 }
2106 }
2103
2107
2104 div.search-feedback-items {
2108 div.search-feedback-items {
2105 display: inline-block;
2109 display: inline-block;
2106 padding:0px 0px 0px 96px;
2110 padding:0px 0px 0px 96px;
2107 }
2111 }
2108
2112
2109 div.search-code-body {
2113 div.search-code-body {
2110 background-color: #ffffff; padding: 5px 0 5px 10px;
2114 background-color: #ffffff; padding: 5px 0 5px 10px;
2111 pre {
2115 pre {
2112 .match { background-color: #faffa6;}
2116 .match { background-color: #faffa6;}
2113 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2117 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2114 }
2118 }
2115 }
2119 }
2116
2120
2117 .expand_commit.search {
2121 .expand_commit.search {
2118 .show_more.open {
2122 .show_more.open {
2119 height: auto;
2123 height: auto;
2120 max-height: none;
2124 max-height: none;
2121 }
2125 }
2122 }
2126 }
2123
2127
2124 .search-results {
2128 .search-results {
2125
2129
2126 h2 {
2130 h2 {
2127 margin-bottom: 0;
2131 margin-bottom: 0;
2128 }
2132 }
2129 .codeblock {
2133 .codeblock {
2130 border: none;
2134 border: none;
2131 background: transparent;
2135 background: transparent;
2132 }
2136 }
2133
2137
2134 .codeblock-header {
2138 .codeblock-header {
2135 border: none;
2139 border: none;
2136 background: transparent;
2140 background: transparent;
2137 }
2141 }
2138
2142
2139 .code-body {
2143 .code-body {
2140 border: @border-thickness solid @border-default-color;
2144 border: @border-thickness solid @border-default-color;
2141 .border-radius(@border-radius);
2145 .border-radius(@border-radius);
2142 }
2146 }
2143
2147
2144 .td-commit {
2148 .td-commit {
2145 &:extend(pre);
2149 &:extend(pre);
2146 border-bottom: @border-thickness solid @border-default-color;
2150 border-bottom: @border-thickness solid @border-default-color;
2147 }
2151 }
2148
2152
2149 .message {
2153 .message {
2150 height: auto;
2154 height: auto;
2151 max-width: 350px;
2155 max-width: 350px;
2152 white-space: normal;
2156 white-space: normal;
2153 text-overflow: initial;
2157 text-overflow: initial;
2154 overflow: visible;
2158 overflow: visible;
2155
2159
2156 .match { background-color: #faffa6;}
2160 .match { background-color: #faffa6;}
2157 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2161 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2158 }
2162 }
2159
2163
2160 }
2164 }
2161
2165
2162 table.rctable td.td-search-results div {
2166 table.rctable td.td-search-results div {
2163 max-width: 100%;
2167 max-width: 100%;
2164 }
2168 }
2165
2169
2166 #tip-box, .tip-box{
2170 #tip-box, .tip-box{
2167 padding: @menupadding/2;
2171 padding: @menupadding/2;
2168 display: block;
2172 display: block;
2169 border: @border-thickness solid @border-highlight-color;
2173 border: @border-thickness solid @border-highlight-color;
2170 .border-radius(@border-radius);
2174 .border-radius(@border-radius);
2171 background-color: white;
2175 background-color: white;
2172 z-index: 99;
2176 z-index: 99;
2173 white-space: pre-wrap;
2177 white-space: pre-wrap;
2174 }
2178 }
2175
2179
2176 #linktt {
2180 #linktt {
2177 width: 79px;
2181 width: 79px;
2178 }
2182 }
2179
2183
2180 #help_kb .modal-content{
2184 #help_kb .modal-content{
2181 max-width: 750px;
2185 max-width: 750px;
2182 margin: 10% auto;
2186 margin: 10% auto;
2183
2187
2184 table{
2188 table{
2185 td,th{
2189 td,th{
2186 border-bottom: none;
2190 border-bottom: none;
2187 line-height: 2.5em;
2191 line-height: 2.5em;
2188 }
2192 }
2189 th{
2193 th{
2190 padding-bottom: @textmargin/2;
2194 padding-bottom: @textmargin/2;
2191 }
2195 }
2192 td.keys{
2196 td.keys{
2193 text-align: center;
2197 text-align: center;
2194 }
2198 }
2195 }
2199 }
2196
2200
2197 .block-left{
2201 .block-left{
2198 width: 45%;
2202 width: 45%;
2199 margin-right: 5%;
2203 margin-right: 5%;
2200 }
2204 }
2201 .modal-footer{
2205 .modal-footer{
2202 clear: both;
2206 clear: both;
2203 }
2207 }
2204 .key.tag{
2208 .key.tag{
2205 padding: 0.5em;
2209 padding: 0.5em;
2206 background-color: @rcblue;
2210 background-color: @rcblue;
2207 color: white;
2211 color: white;
2208 border-color: @rcblue;
2212 border-color: @rcblue;
2209 .box-shadow(none);
2213 .box-shadow(none);
2210 }
2214 }
2211 }
2215 }
2212
2216
2213
2217
2214
2218
2215 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2219 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2216
2220
2217 @import 'statistics-graph';
2221 @import 'statistics-graph';
2218 @import 'tables';
2222 @import 'tables';
2219 @import 'forms';
2223 @import 'forms';
2220 @import 'diff';
2224 @import 'diff';
2221 @import 'summary';
2225 @import 'summary';
2222 @import 'navigation';
2226 @import 'navigation';
2223
2227
2224 //--- SHOW/HIDE SECTIONS --//
2228 //--- SHOW/HIDE SECTIONS --//
2225
2229
2226 .btn-collapse {
2230 .btn-collapse {
2227 float: right;
2231 float: right;
2228 text-align: right;
2232 text-align: right;
2229 font-family: @text-light;
2233 font-family: @text-light;
2230 font-size: @basefontsize;
2234 font-size: @basefontsize;
2231 cursor: pointer;
2235 cursor: pointer;
2232 border: none;
2236 border: none;
2233 color: @rcblue;
2237 color: @rcblue;
2234 }
2238 }
2235
2239
2236 table.rctable,
2240 table.rctable,
2237 table.dataTable {
2241 table.dataTable {
2238 .btn-collapse {
2242 .btn-collapse {
2239 float: right;
2243 float: right;
2240 text-align: right;
2244 text-align: right;
2241 }
2245 }
2242 }
2246 }
2243
2247
2244
2248
2245 // TODO: johbo: Fix for IE10, this avoids that we see a border
2249 // TODO: johbo: Fix for IE10, this avoids that we see a border
2246 // and padding around checkboxes and radio boxes. Move to the right place,
2250 // and padding around checkboxes and radio boxes. Move to the right place,
2247 // or better: Remove this once we did the form refactoring.
2251 // or better: Remove this once we did the form refactoring.
2248 input[type=checkbox],
2252 input[type=checkbox],
2249 input[type=radio] {
2253 input[type=radio] {
2250 padding: 0;
2254 padding: 0;
2251 border: none;
2255 border: none;
2252 }
2256 }
2253
2257
2254 .toggle-ajax-spinner{
2258 .toggle-ajax-spinner{
2255 height: 16px;
2259 height: 16px;
2256 width: 16px;
2260 width: 16px;
2257 }
2261 }
@@ -1,37 +1,44 b''
1
1
2 <div class="pull-request-wrap">
2 <div class="pull-request-wrap">
3
3
4
5 % if c.pr_merge_possible:
6 <h2 class="merge-status">
7 <span class="merge-icon success"><i class="icon-true"></i></span>
8 ${_('This pull request can be merged automatically.')}
9 </h2>
10 % else:
11 <h2 class="merge-status">
12 <span class="merge-icon warning"><i class="icon-false"></i></span>
13 ${_('Merge is not currently possible because of below failed checks.')}
14 </h2>
15 % endif
16
4 <ul>
17 <ul>
5 % for pr_check_type, pr_check_msg in c.pr_merge_checks:
18 % for pr_check_type, pr_check_msg in c.pr_merge_errors:
6 <li>
19 <li>
7 <span class="merge-message ${pr_check_type}" data-role="merge-message">
20 <span class="merge-message ${pr_check_type}" data-role="merge-message">
8 % if pr_check_type in ['success']:
21 - ${pr_check_msg}
9 <i class="icon-true"></i>
10 % else:
11 <i class="icon-false"></i>
12 % endif
13 ${pr_check_msg}
14 </span>
22 </span>
15 </li>
23 </li>
16 % endfor
24 % endfor
17 </ul>
25 </ul>
18
26
19 <div class="pull-request-merge-actions">
27 <div class="pull-request-merge-actions">
20 % if c.allowed_to_merge:
28 % if c.allowed_to_merge:
21 <div class="pull-right">
29 <div class="pull-right">
22 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
30 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
23 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
31 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
24 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
32 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
25 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
33 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
26 ${h.end_form()}
34 ${h.end_form()}
27 </div>
35 </div>
28 % elif c.rhodecode_user.username != h.DEFAULT_USER:
36 % elif c.rhodecode_user.username != h.DEFAULT_USER:
29 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
37 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
30 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
38 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
31 % else:
39 % else:
32 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
40 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
33 % endif
41 % endif
34 </div>
42 </div>
35
36 </div>
43 </div>
37
44
@@ -1,1064 +1,1067 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 from webob.exc import HTTPNotFound
23 from webob.exc import HTTPNotFound
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.vcs.nodes import FileNode
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)
29 PullRequest, ChangesetStatus, UserLog, Notification)
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.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
34 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
34 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
35 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
36
36
37
37
38 @pytest.mark.usefixtures('app', 'autologin_user')
38 @pytest.mark.usefixtures('app', 'autologin_user')
39 @pytest.mark.backends("git", "hg")
39 @pytest.mark.backends("git", "hg")
40 class TestPullrequestsController:
40 class TestPullrequestsController:
41
41
42 def test_index(self, backend):
42 def test_index(self, backend):
43 self.app.get(url(
43 self.app.get(url(
44 controller='pullrequests', action='index',
44 controller='pullrequests', action='index',
45 repo_name=backend.repo_name))
45 repo_name=backend.repo_name))
46
46
47 def test_option_menu_create_pull_request_exists(self, backend):
47 def test_option_menu_create_pull_request_exists(self, backend):
48 repo_name = backend.repo_name
48 repo_name = backend.repo_name
49 response = self.app.get(url('summary_home', repo_name=repo_name))
49 response = self.app.get(url('summary_home', repo_name=repo_name))
50
50
51 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
51 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
52 'pullrequest', repo_name=repo_name)
52 'pullrequest', repo_name=repo_name)
53 response.mustcontain(create_pr_link)
53 response.mustcontain(create_pr_link)
54
54
55 def test_global_redirect_of_pr(self, backend, pr_util):
55 def test_global_redirect_of_pr(self, backend, pr_util):
56 pull_request = pr_util.create_pull_request()
56 pull_request = pr_util.create_pull_request()
57
57
58 response = self.app.get(
58 response = self.app.get(
59 url('pull_requests_global',
59 url('pull_requests_global',
60 pull_request_id=pull_request.pull_request_id))
60 pull_request_id=pull_request.pull_request_id))
61
61
62 repo_name = pull_request.target_repo.repo_name
62 repo_name = pull_request.target_repo.repo_name
63 redirect_url = url('pullrequest_show', repo_name=repo_name,
63 redirect_url = url('pullrequest_show', repo_name=repo_name,
64 pull_request_id=pull_request.pull_request_id)
64 pull_request_id=pull_request.pull_request_id)
65 assert response.status == '302 Found'
65 assert response.status == '302 Found'
66 assert redirect_url in response.location
66 assert redirect_url in response.location
67
67
68 def test_create_pr_form_with_raw_commit_id(self, backend):
68 def test_create_pr_form_with_raw_commit_id(self, backend):
69 repo = backend.repo
69 repo = backend.repo
70
70
71 self.app.get(
71 self.app.get(
72 url(controller='pullrequests', action='index',
72 url(controller='pullrequests', action='index',
73 repo_name=repo.repo_name,
73 repo_name=repo.repo_name,
74 commit=repo.get_commit().raw_id),
74 commit=repo.get_commit().raw_id),
75 status=200)
75 status=200)
76
76
77 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
77 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
78 def test_show(self, pr_util, pr_merge_enabled):
78 def test_show(self, pr_util, pr_merge_enabled):
79 pull_request = pr_util.create_pull_request(
79 pull_request = pr_util.create_pull_request(
80 mergeable=pr_merge_enabled, enable_notifications=False)
80 mergeable=pr_merge_enabled, enable_notifications=False)
81
81
82 response = self.app.get(url(
82 response = self.app.get(url(
83 controller='pullrequests', action='show',
83 controller='pullrequests', action='show',
84 repo_name=pull_request.target_repo.scm_instance().name,
84 repo_name=pull_request.target_repo.scm_instance().name,
85 pull_request_id=str(pull_request.pull_request_id)))
85 pull_request_id=str(pull_request.pull_request_id)))
86
86
87 for commit_id in pull_request.revisions:
87 for commit_id in pull_request.revisions:
88 response.mustcontain(commit_id)
88 response.mustcontain(commit_id)
89
89
90 assert pull_request.target_ref_parts.type in response
90 assert pull_request.target_ref_parts.type in response
91 assert pull_request.target_ref_parts.name in response
91 assert pull_request.target_ref_parts.name in response
92 target_clone_url = pull_request.target_repo.clone_url()
92 target_clone_url = pull_request.target_repo.clone_url()
93 assert target_clone_url in response
93 assert target_clone_url in response
94
94
95 assert 'class="pull-request-merge"' in response
95 assert 'class="pull-request-merge"' in response
96 assert (
96 assert (
97 'Server-side pull request merging is disabled.'
97 'Server-side pull request merging is disabled.'
98 in response) != pr_merge_enabled
98 in response) != pr_merge_enabled
99
99
100 def test_close_status_visibility(self, pr_util, csrf_token):
100 def test_close_status_visibility(self, pr_util, csrf_token):
101 from rhodecode.tests.functional.test_login import login_url, logut_url
101 from rhodecode.tests.functional.test_login import login_url, logut_url
102 # Logout
102 # Logout
103 response = self.app.post(
103 response = self.app.post(
104 logut_url,
104 logut_url,
105 params={'csrf_token': csrf_token})
105 params={'csrf_token': csrf_token})
106 # Login as regular user
106 # Login as regular user
107 response = self.app.post(login_url,
107 response = self.app.post(login_url,
108 {'username': 'test_regular',
108 {'username': 'test_regular',
109 'password': 'test12'})
109 'password': 'test12'})
110
110
111 pull_request = pr_util.create_pull_request(author='test_regular')
111 pull_request = pr_util.create_pull_request(author='test_regular')
112
112
113 response = self.app.get(url(
113 response = self.app.get(url(
114 controller='pullrequests', action='show',
114 controller='pullrequests', action='show',
115 repo_name=pull_request.target_repo.scm_instance().name,
115 repo_name=pull_request.target_repo.scm_instance().name,
116 pull_request_id=str(pull_request.pull_request_id)))
116 pull_request_id=str(pull_request.pull_request_id)))
117
117
118 assert 'Server-side pull request merging is disabled.' in response
118 assert 'Server-side pull request merging is disabled.' in response
119 assert 'value="forced_closed"' in response
119 assert 'value="forced_closed"' in response
120
120
121 def test_show_invalid_commit_id(self, pr_util):
121 def test_show_invalid_commit_id(self, pr_util):
122 # Simulating invalid revisions which will cause a lookup error
122 # Simulating invalid revisions which will cause a lookup error
123 pull_request = pr_util.create_pull_request()
123 pull_request = pr_util.create_pull_request()
124 pull_request.revisions = ['invalid']
124 pull_request.revisions = ['invalid']
125 Session().add(pull_request)
125 Session().add(pull_request)
126 Session().commit()
126 Session().commit()
127
127
128 response = self.app.get(url(
128 response = self.app.get(url(
129 controller='pullrequests', action='show',
129 controller='pullrequests', action='show',
130 repo_name=pull_request.target_repo.scm_instance().name,
130 repo_name=pull_request.target_repo.scm_instance().name,
131 pull_request_id=str(pull_request.pull_request_id)))
131 pull_request_id=str(pull_request.pull_request_id)))
132
132
133 for commit_id in pull_request.revisions:
133 for commit_id in pull_request.revisions:
134 response.mustcontain(commit_id)
134 response.mustcontain(commit_id)
135
135
136 def test_show_invalid_source_reference(self, pr_util):
136 def test_show_invalid_source_reference(self, pr_util):
137 pull_request = pr_util.create_pull_request()
137 pull_request = pr_util.create_pull_request()
138 pull_request.source_ref = 'branch:b:invalid'
138 pull_request.source_ref = 'branch:b:invalid'
139 Session().add(pull_request)
139 Session().add(pull_request)
140 Session().commit()
140 Session().commit()
141
141
142 self.app.get(url(
142 self.app.get(url(
143 controller='pullrequests', action='show',
143 controller='pullrequests', action='show',
144 repo_name=pull_request.target_repo.scm_instance().name,
144 repo_name=pull_request.target_repo.scm_instance().name,
145 pull_request_id=str(pull_request.pull_request_id)))
145 pull_request_id=str(pull_request.pull_request_id)))
146
146
147 def test_edit_title_description(self, pr_util, csrf_token):
147 def test_edit_title_description(self, pr_util, csrf_token):
148 pull_request = pr_util.create_pull_request()
148 pull_request = pr_util.create_pull_request()
149 pull_request_id = pull_request.pull_request_id
149 pull_request_id = pull_request.pull_request_id
150
150
151 response = self.app.post(
151 response = self.app.post(
152 url(controller='pullrequests', action='update',
152 url(controller='pullrequests', action='update',
153 repo_name=pull_request.target_repo.repo_name,
153 repo_name=pull_request.target_repo.repo_name,
154 pull_request_id=str(pull_request_id)),
154 pull_request_id=str(pull_request_id)),
155 params={
155 params={
156 'edit_pull_request': 'true',
156 'edit_pull_request': 'true',
157 '_method': 'put',
157 '_method': 'put',
158 'title': 'New title',
158 'title': 'New title',
159 'description': 'New description',
159 'description': 'New description',
160 'csrf_token': csrf_token})
160 'csrf_token': csrf_token})
161
161
162 assert_session_flash(
162 assert_session_flash(
163 response, u'Pull request title & description updated.',
163 response, u'Pull request title & description updated.',
164 category='success')
164 category='success')
165
165
166 pull_request = PullRequest.get(pull_request_id)
166 pull_request = PullRequest.get(pull_request_id)
167 assert pull_request.title == 'New title'
167 assert pull_request.title == 'New title'
168 assert pull_request.description == 'New description'
168 assert pull_request.description == 'New description'
169
169
170 def test_edit_title_description_closed(self, pr_util, csrf_token):
170 def test_edit_title_description_closed(self, pr_util, csrf_token):
171 pull_request = pr_util.create_pull_request()
171 pull_request = pr_util.create_pull_request()
172 pull_request_id = pull_request.pull_request_id
172 pull_request_id = pull_request.pull_request_id
173 pr_util.close()
173 pr_util.close()
174
174
175 response = self.app.post(
175 response = self.app.post(
176 url(controller='pullrequests', action='update',
176 url(controller='pullrequests', action='update',
177 repo_name=pull_request.target_repo.repo_name,
177 repo_name=pull_request.target_repo.repo_name,
178 pull_request_id=str(pull_request_id)),
178 pull_request_id=str(pull_request_id)),
179 params={
179 params={
180 'edit_pull_request': 'true',
180 'edit_pull_request': 'true',
181 '_method': 'put',
181 '_method': 'put',
182 'title': 'New title',
182 'title': 'New title',
183 'description': 'New description',
183 'description': 'New description',
184 'csrf_token': csrf_token})
184 'csrf_token': csrf_token})
185
185
186 assert_session_flash(
186 assert_session_flash(
187 response, u'Cannot update closed pull requests.',
187 response, u'Cannot update closed pull requests.',
188 category='error')
188 category='error')
189
189
190 def test_update_invalid_source_reference(self, pr_util, csrf_token):
190 def test_update_invalid_source_reference(self, pr_util, csrf_token):
191 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
191 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
192
192
193 pull_request = pr_util.create_pull_request()
193 pull_request = pr_util.create_pull_request()
194 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
194 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
195 Session().add(pull_request)
195 Session().add(pull_request)
196 Session().commit()
196 Session().commit()
197
197
198 pull_request_id = pull_request.pull_request_id
198 pull_request_id = pull_request.pull_request_id
199
199
200 response = self.app.post(
200 response = self.app.post(
201 url(controller='pullrequests', action='update',
201 url(controller='pullrequests', action='update',
202 repo_name=pull_request.target_repo.repo_name,
202 repo_name=pull_request.target_repo.repo_name,
203 pull_request_id=str(pull_request_id)),
203 pull_request_id=str(pull_request_id)),
204 params={'update_commits': 'true', '_method': 'put',
204 params={'update_commits': 'true', '_method': 'put',
205 'csrf_token': csrf_token})
205 'csrf_token': csrf_token})
206
206
207 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
207 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
208 UpdateFailureReason.MISSING_SOURCE_REF]
208 UpdateFailureReason.MISSING_SOURCE_REF]
209 assert_session_flash(response, expected_msg, category='error')
209 assert_session_flash(response, expected_msg, category='error')
210
210
211 def test_missing_target_reference(self, pr_util, csrf_token):
211 def test_missing_target_reference(self, pr_util, csrf_token):
212 from rhodecode.lib.vcs.backends.base import MergeFailureReason
212 from rhodecode.lib.vcs.backends.base import MergeFailureReason
213 pull_request = pr_util.create_pull_request(
213 pull_request = pr_util.create_pull_request(
214 approved=True, mergeable=True)
214 approved=True, mergeable=True)
215 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
215 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
216 Session().add(pull_request)
216 Session().add(pull_request)
217 Session().commit()
217 Session().commit()
218
218
219 pull_request_id = pull_request.pull_request_id
219 pull_request_id = pull_request.pull_request_id
220 pull_request_url = url(
220 pull_request_url = url(
221 controller='pullrequests', action='show',
221 controller='pullrequests', action='show',
222 repo_name=pull_request.target_repo.repo_name,
222 repo_name=pull_request.target_repo.repo_name,
223 pull_request_id=str(pull_request_id))
223 pull_request_id=str(pull_request_id))
224
224
225 response = self.app.get(pull_request_url)
225 response = self.app.get(pull_request_url)
226
226
227 assertr = AssertResponse(response)
227 assertr = AssertResponse(response)
228 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
228 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
229 MergeFailureReason.MISSING_TARGET_REF]
229 MergeFailureReason.MISSING_TARGET_REF]
230 assertr.element_contains(
230 assertr.element_contains(
231 'span[data-role="merge-message"]', str(expected_msg))
231 'span[data-role="merge-message"]', str(expected_msg))
232
232
233 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
233 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
234 pull_request = pr_util.create_pull_request(approved=True)
234 pull_request = pr_util.create_pull_request(approved=True)
235 pull_request_id = pull_request.pull_request_id
235 pull_request_id = pull_request.pull_request_id
236 author = pull_request.user_id
236 author = pull_request.user_id
237 repo = pull_request.target_repo.repo_id
237 repo = pull_request.target_repo.repo_id
238
238
239 self.app.post(
239 self.app.post(
240 url(controller='pullrequests',
240 url(controller='pullrequests',
241 action='comment',
241 action='comment',
242 repo_name=pull_request.target_repo.scm_instance().name,
242 repo_name=pull_request.target_repo.scm_instance().name,
243 pull_request_id=str(pull_request_id)),
243 pull_request_id=str(pull_request_id)),
244 params={
244 params={
245 'changeset_status':
245 'changeset_status':
246 ChangesetStatus.STATUS_APPROVED + '_closed',
246 ChangesetStatus.STATUS_APPROVED + '_closed',
247 'change_changeset_status': 'on',
247 'change_changeset_status': 'on',
248 'text': '',
248 'text': '',
249 'csrf_token': csrf_token},
249 'csrf_token': csrf_token},
250 status=302)
250 status=302)
251
251
252 action = 'user_closed_pull_request:%d' % pull_request_id
252 action = 'user_closed_pull_request:%d' % pull_request_id
253 journal = UserLog.query()\
253 journal = UserLog.query()\
254 .filter(UserLog.user_id == author)\
254 .filter(UserLog.user_id == author)\
255 .filter(UserLog.repository_id == repo)\
255 .filter(UserLog.repository_id == repo)\
256 .filter(UserLog.action == action)\
256 .filter(UserLog.action == action)\
257 .all()
257 .all()
258 assert len(journal) == 1
258 assert len(journal) == 1
259
259
260 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
260 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
261 pull_request = pr_util.create_pull_request()
261 pull_request = pr_util.create_pull_request()
262 pull_request_id = pull_request.pull_request_id
262 pull_request_id = pull_request.pull_request_id
263 response = self.app.post(
263 response = self.app.post(
264 url(controller='pullrequests',
264 url(controller='pullrequests',
265 action='update',
265 action='update',
266 repo_name=pull_request.target_repo.scm_instance().name,
266 repo_name=pull_request.target_repo.scm_instance().name,
267 pull_request_id=str(pull_request.pull_request_id)),
267 pull_request_id=str(pull_request.pull_request_id)),
268 params={'close_pull_request': 'true', '_method': 'put',
268 params={'close_pull_request': 'true', '_method': 'put',
269 'csrf_token': csrf_token})
269 'csrf_token': csrf_token})
270
270
271 pull_request = PullRequest.get(pull_request_id)
271 pull_request = PullRequest.get(pull_request_id)
272
272
273 assert response.json is True
273 assert response.json is True
274 assert pull_request.is_closed()
274 assert pull_request.is_closed()
275
275
276 # check only the latest status, not the review status
276 # check only the latest status, not the review status
277 status = ChangesetStatusModel().get_status(
277 status = ChangesetStatusModel().get_status(
278 pull_request.source_repo, pull_request=pull_request)
278 pull_request.source_repo, pull_request=pull_request)
279 assert status == ChangesetStatus.STATUS_REJECTED
279 assert status == ChangesetStatus.STATUS_REJECTED
280
280
281 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
281 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
282 pull_request = pr_util.create_pull_request()
282 pull_request = pr_util.create_pull_request()
283 pull_request_id = pull_request.pull_request_id
283 pull_request_id = pull_request.pull_request_id
284 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
284 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
285 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
285 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
286 author = pull_request.user_id
286 author = pull_request.user_id
287 repo = pull_request.target_repo.repo_id
287 repo = pull_request.target_repo.repo_id
288 self.app.post(
288 self.app.post(
289 url(controller='pullrequests',
289 url(controller='pullrequests',
290 action='comment',
290 action='comment',
291 repo_name=pull_request.target_repo.scm_instance().name,
291 repo_name=pull_request.target_repo.scm_instance().name,
292 pull_request_id=str(pull_request_id)),
292 pull_request_id=str(pull_request_id)),
293 params={
293 params={
294 'changeset_status': 'forced_closed',
294 'changeset_status': 'forced_closed',
295 'csrf_token': csrf_token},
295 'csrf_token': csrf_token},
296 status=302)
296 status=302)
297
297
298 pull_request = PullRequest.get(pull_request_id)
298 pull_request = PullRequest.get(pull_request_id)
299
299
300 action = 'user_closed_pull_request:%d' % pull_request_id
300 action = 'user_closed_pull_request:%d' % pull_request_id
301 journal = UserLog.query().filter(
301 journal = UserLog.query().filter(
302 UserLog.user_id == author,
302 UserLog.user_id == author,
303 UserLog.repository_id == repo,
303 UserLog.repository_id == repo,
304 UserLog.action == action).all()
304 UserLog.action == action).all()
305 assert len(journal) == 1
305 assert len(journal) == 1
306
306
307 # check only the latest status, not the review status
307 # check only the latest status, not the review status
308 status = ChangesetStatusModel().get_status(
308 status = ChangesetStatusModel().get_status(
309 pull_request.source_repo, pull_request=pull_request)
309 pull_request.source_repo, pull_request=pull_request)
310 assert status == ChangesetStatus.STATUS_REJECTED
310 assert status == ChangesetStatus.STATUS_REJECTED
311
311
312 def test_create_pull_request(self, backend, csrf_token):
312 def test_create_pull_request(self, backend, csrf_token):
313 commits = [
313 commits = [
314 {'message': 'ancestor'},
314 {'message': 'ancestor'},
315 {'message': 'change'},
315 {'message': 'change'},
316 {'message': 'change2'},
316 {'message': 'change2'},
317 ]
317 ]
318 commit_ids = backend.create_master_repo(commits)
318 commit_ids = backend.create_master_repo(commits)
319 target = backend.create_repo(heads=['ancestor'])
319 target = backend.create_repo(heads=['ancestor'])
320 source = backend.create_repo(heads=['change2'])
320 source = backend.create_repo(heads=['change2'])
321
321
322 response = self.app.post(
322 response = self.app.post(
323 url(
323 url(
324 controller='pullrequests',
324 controller='pullrequests',
325 action='create',
325 action='create',
326 repo_name=source.repo_name
326 repo_name=source.repo_name
327 ),
327 ),
328 [
328 [
329 ('source_repo', source.repo_name),
329 ('source_repo', source.repo_name),
330 ('source_ref', 'branch:default:' + commit_ids['change2']),
330 ('source_ref', 'branch:default:' + commit_ids['change2']),
331 ('target_repo', target.repo_name),
331 ('target_repo', target.repo_name),
332 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
332 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
333 ('pullrequest_desc', 'Description'),
333 ('pullrequest_desc', 'Description'),
334 ('pullrequest_title', 'Title'),
334 ('pullrequest_title', 'Title'),
335 ('__start__', 'review_members:sequence'),
335 ('__start__', 'review_members:sequence'),
336 ('__start__', 'reviewer:mapping'),
336 ('__start__', 'reviewer:mapping'),
337 ('user_id', '1'),
337 ('user_id', '1'),
338 ('__start__', 'reasons:sequence'),
338 ('__start__', 'reasons:sequence'),
339 ('reason', 'Some reason'),
339 ('reason', 'Some reason'),
340 ('__end__', 'reasons:sequence'),
340 ('__end__', 'reasons:sequence'),
341 ('__end__', 'reviewer:mapping'),
341 ('__end__', 'reviewer:mapping'),
342 ('__end__', 'review_members:sequence'),
342 ('__end__', 'review_members:sequence'),
343 ('__start__', 'revisions:sequence'),
343 ('__start__', 'revisions:sequence'),
344 ('revisions', commit_ids['change']),
344 ('revisions', commit_ids['change']),
345 ('revisions', commit_ids['change2']),
345 ('revisions', commit_ids['change2']),
346 ('__end__', 'revisions:sequence'),
346 ('__end__', 'revisions:sequence'),
347 ('user', ''),
347 ('user', ''),
348 ('csrf_token', csrf_token),
348 ('csrf_token', csrf_token),
349 ],
349 ],
350 status=302)
350 status=302)
351
351
352 location = response.headers['Location']
352 location = response.headers['Location']
353 pull_request_id = int(location.rsplit('/', 1)[1])
353 pull_request_id = int(location.rsplit('/', 1)[1])
354 pull_request = PullRequest.get(pull_request_id)
354 pull_request = PullRequest.get(pull_request_id)
355
355
356 # check that we have now both revisions
356 # check that we have now both revisions
357 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
357 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
358 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
358 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
359 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
359 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
360 assert pull_request.target_ref == expected_target_ref
360 assert pull_request.target_ref == expected_target_ref
361
361
362 def test_reviewer_notifications(self, backend, csrf_token):
362 def test_reviewer_notifications(self, backend, csrf_token):
363 # We have to use the app.post for this test so it will create the
363 # We have to use the app.post for this test so it will create the
364 # notifications properly with the new PR
364 # notifications properly with the new PR
365 commits = [
365 commits = [
366 {'message': 'ancestor',
366 {'message': 'ancestor',
367 'added': [FileNode('file_A', content='content_of_ancestor')]},
367 'added': [FileNode('file_A', content='content_of_ancestor')]},
368 {'message': 'change',
368 {'message': 'change',
369 'added': [FileNode('file_a', content='content_of_change')]},
369 'added': [FileNode('file_a', content='content_of_change')]},
370 {'message': 'change-child'},
370 {'message': 'change-child'},
371 {'message': 'ancestor-child', 'parents': ['ancestor'],
371 {'message': 'ancestor-child', 'parents': ['ancestor'],
372 'added': [
372 'added': [
373 FileNode('file_B', content='content_of_ancestor_child')]},
373 FileNode('file_B', content='content_of_ancestor_child')]},
374 {'message': 'ancestor-child-2'},
374 {'message': 'ancestor-child-2'},
375 ]
375 ]
376 commit_ids = backend.create_master_repo(commits)
376 commit_ids = backend.create_master_repo(commits)
377 target = backend.create_repo(heads=['ancestor-child'])
377 target = backend.create_repo(heads=['ancestor-child'])
378 source = backend.create_repo(heads=['change'])
378 source = backend.create_repo(heads=['change'])
379
379
380 response = self.app.post(
380 response = self.app.post(
381 url(
381 url(
382 controller='pullrequests',
382 controller='pullrequests',
383 action='create',
383 action='create',
384 repo_name=source.repo_name
384 repo_name=source.repo_name
385 ),
385 ),
386 [
386 [
387 ('source_repo', source.repo_name),
387 ('source_repo', source.repo_name),
388 ('source_ref', 'branch:default:' + commit_ids['change']),
388 ('source_ref', 'branch:default:' + commit_ids['change']),
389 ('target_repo', target.repo_name),
389 ('target_repo', target.repo_name),
390 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
390 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
391 ('pullrequest_desc', 'Description'),
391 ('pullrequest_desc', 'Description'),
392 ('pullrequest_title', 'Title'),
392 ('pullrequest_title', 'Title'),
393 ('__start__', 'review_members:sequence'),
393 ('__start__', 'review_members:sequence'),
394 ('__start__', 'reviewer:mapping'),
394 ('__start__', 'reviewer:mapping'),
395 ('user_id', '2'),
395 ('user_id', '2'),
396 ('__start__', 'reasons:sequence'),
396 ('__start__', 'reasons:sequence'),
397 ('reason', 'Some reason'),
397 ('reason', 'Some reason'),
398 ('__end__', 'reasons:sequence'),
398 ('__end__', 'reasons:sequence'),
399 ('__end__', 'reviewer:mapping'),
399 ('__end__', 'reviewer:mapping'),
400 ('__end__', 'review_members:sequence'),
400 ('__end__', 'review_members:sequence'),
401 ('__start__', 'revisions:sequence'),
401 ('__start__', 'revisions:sequence'),
402 ('revisions', commit_ids['change']),
402 ('revisions', commit_ids['change']),
403 ('__end__', 'revisions:sequence'),
403 ('__end__', 'revisions:sequence'),
404 ('user', ''),
404 ('user', ''),
405 ('csrf_token', csrf_token),
405 ('csrf_token', csrf_token),
406 ],
406 ],
407 status=302)
407 status=302)
408
408
409 location = response.headers['Location']
409 location = response.headers['Location']
410 pull_request_id = int(location.rsplit('/', 1)[1])
410 pull_request_id = int(location.rsplit('/', 1)[1])
411 pull_request = PullRequest.get(pull_request_id)
411 pull_request = PullRequest.get(pull_request_id)
412
412
413 # Check that a notification was made
413 # Check that a notification was made
414 notifications = Notification.query()\
414 notifications = Notification.query()\
415 .filter(Notification.created_by == pull_request.author.user_id,
415 .filter(Notification.created_by == pull_request.author.user_id,
416 Notification.type_ == Notification.TYPE_PULL_REQUEST,
416 Notification.type_ == Notification.TYPE_PULL_REQUEST,
417 Notification.subject.contains("wants you to review "
417 Notification.subject.contains("wants you to review "
418 "pull request #%d"
418 "pull request #%d"
419 % pull_request_id))
419 % pull_request_id))
420 assert len(notifications.all()) == 1
420 assert len(notifications.all()) == 1
421
421
422 # Change reviewers and check that a notification was made
422 # Change reviewers and check that a notification was made
423 PullRequestModel().update_reviewers(
423 PullRequestModel().update_reviewers(
424 pull_request.pull_request_id, [(1, [])])
424 pull_request.pull_request_id, [(1, [])])
425 assert len(notifications.all()) == 2
425 assert len(notifications.all()) == 2
426
426
427 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
427 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
428 csrf_token):
428 csrf_token):
429 commits = [
429 commits = [
430 {'message': 'ancestor',
430 {'message': 'ancestor',
431 'added': [FileNode('file_A', content='content_of_ancestor')]},
431 'added': [FileNode('file_A', content='content_of_ancestor')]},
432 {'message': 'change',
432 {'message': 'change',
433 'added': [FileNode('file_a', content='content_of_change')]},
433 'added': [FileNode('file_a', content='content_of_change')]},
434 {'message': 'change-child'},
434 {'message': 'change-child'},
435 {'message': 'ancestor-child', 'parents': ['ancestor'],
435 {'message': 'ancestor-child', 'parents': ['ancestor'],
436 'added': [
436 'added': [
437 FileNode('file_B', content='content_of_ancestor_child')]},
437 FileNode('file_B', content='content_of_ancestor_child')]},
438 {'message': 'ancestor-child-2'},
438 {'message': 'ancestor-child-2'},
439 ]
439 ]
440 commit_ids = backend.create_master_repo(commits)
440 commit_ids = backend.create_master_repo(commits)
441 target = backend.create_repo(heads=['ancestor-child'])
441 target = backend.create_repo(heads=['ancestor-child'])
442 source = backend.create_repo(heads=['change'])
442 source = backend.create_repo(heads=['change'])
443
443
444 response = self.app.post(
444 response = self.app.post(
445 url(
445 url(
446 controller='pullrequests',
446 controller='pullrequests',
447 action='create',
447 action='create',
448 repo_name=source.repo_name
448 repo_name=source.repo_name
449 ),
449 ),
450 [
450 [
451 ('source_repo', source.repo_name),
451 ('source_repo', source.repo_name),
452 ('source_ref', 'branch:default:' + commit_ids['change']),
452 ('source_ref', 'branch:default:' + commit_ids['change']),
453 ('target_repo', target.repo_name),
453 ('target_repo', target.repo_name),
454 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
454 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
455 ('pullrequest_desc', 'Description'),
455 ('pullrequest_desc', 'Description'),
456 ('pullrequest_title', 'Title'),
456 ('pullrequest_title', 'Title'),
457 ('__start__', 'review_members:sequence'),
457 ('__start__', 'review_members:sequence'),
458 ('__start__', 'reviewer:mapping'),
458 ('__start__', 'reviewer:mapping'),
459 ('user_id', '1'),
459 ('user_id', '1'),
460 ('__start__', 'reasons:sequence'),
460 ('__start__', 'reasons:sequence'),
461 ('reason', 'Some reason'),
461 ('reason', 'Some reason'),
462 ('__end__', 'reasons:sequence'),
462 ('__end__', 'reasons:sequence'),
463 ('__end__', 'reviewer:mapping'),
463 ('__end__', 'reviewer:mapping'),
464 ('__end__', 'review_members:sequence'),
464 ('__end__', 'review_members:sequence'),
465 ('__start__', 'revisions:sequence'),
465 ('__start__', 'revisions:sequence'),
466 ('revisions', commit_ids['change']),
466 ('revisions', commit_ids['change']),
467 ('__end__', 'revisions:sequence'),
467 ('__end__', 'revisions:sequence'),
468 ('user', ''),
468 ('user', ''),
469 ('csrf_token', csrf_token),
469 ('csrf_token', csrf_token),
470 ],
470 ],
471 status=302)
471 status=302)
472
472
473 location = response.headers['Location']
473 location = response.headers['Location']
474 pull_request_id = int(location.rsplit('/', 1)[1])
474 pull_request_id = int(location.rsplit('/', 1)[1])
475 pull_request = PullRequest.get(pull_request_id)
475 pull_request = PullRequest.get(pull_request_id)
476
476
477 # target_ref has to point to the ancestor's commit_id in order to
477 # target_ref has to point to the ancestor's commit_id in order to
478 # show the correct diff
478 # show the correct diff
479 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
479 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
480 assert pull_request.target_ref == expected_target_ref
480 assert pull_request.target_ref == expected_target_ref
481
481
482 # Check generated diff contents
482 # Check generated diff contents
483 response = response.follow()
483 response = response.follow()
484 assert 'content_of_ancestor' not in response.body
484 assert 'content_of_ancestor' not in response.body
485 assert 'content_of_ancestor-child' not in response.body
485 assert 'content_of_ancestor-child' not in response.body
486 assert 'content_of_change' in response.body
486 assert 'content_of_change' in response.body
487
487
488 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
488 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
489 # Clear any previous calls to rcextensions
489 # Clear any previous calls to rcextensions
490 rhodecode.EXTENSIONS.calls.clear()
490 rhodecode.EXTENSIONS.calls.clear()
491
491
492 pull_request = pr_util.create_pull_request(
492 pull_request = pr_util.create_pull_request(
493 approved=True, mergeable=True)
493 approved=True, mergeable=True)
494 pull_request_id = pull_request.pull_request_id
494 pull_request_id = pull_request.pull_request_id
495 repo_name = pull_request.target_repo.scm_instance().name,
495 repo_name = pull_request.target_repo.scm_instance().name,
496
496
497 response = self.app.post(
497 response = self.app.post(
498 url(controller='pullrequests',
498 url(controller='pullrequests',
499 action='merge',
499 action='merge',
500 repo_name=str(repo_name[0]),
500 repo_name=str(repo_name[0]),
501 pull_request_id=str(pull_request_id)),
501 pull_request_id=str(pull_request_id)),
502 params={'csrf_token': csrf_token}).follow()
502 params={'csrf_token': csrf_token}).follow()
503
503
504 pull_request = PullRequest.get(pull_request_id)
504 pull_request = PullRequest.get(pull_request_id)
505
505
506 assert response.status_int == 200
506 assert response.status_int == 200
507 assert pull_request.is_closed()
507 assert pull_request.is_closed()
508 assert_pull_request_status(
508 assert_pull_request_status(
509 pull_request, ChangesetStatus.STATUS_APPROVED)
509 pull_request, ChangesetStatus.STATUS_APPROVED)
510
510
511 # Check the relevant log entries were added
511 # Check the relevant log entries were added
512 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
512 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
513 actions = [log.action for log in user_logs]
513 actions = [log.action for log in user_logs]
514 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
514 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
515 expected_actions = [
515 expected_actions = [
516 u'user_closed_pull_request:%d' % pull_request_id,
516 u'user_closed_pull_request:%d' % pull_request_id,
517 u'user_merged_pull_request:%d' % pull_request_id,
517 u'user_merged_pull_request:%d' % pull_request_id,
518 # The action below reflect that the post push actions were executed
518 # The action below reflect that the post push actions were executed
519 u'user_commented_pull_request:%d' % pull_request_id,
519 u'user_commented_pull_request:%d' % pull_request_id,
520 u'push:%s' % ','.join(pr_commit_ids),
520 u'push:%s' % ','.join(pr_commit_ids),
521 ]
521 ]
522 assert actions == expected_actions
522 assert actions == expected_actions
523
523
524 # Check post_push rcextension was really executed
524 # Check post_push rcextension was really executed
525 push_calls = rhodecode.EXTENSIONS.calls['post_push']
525 push_calls = rhodecode.EXTENSIONS.calls['post_push']
526 assert len(push_calls) == 1
526 assert len(push_calls) == 1
527 unused_last_call_args, last_call_kwargs = push_calls[0]
527 unused_last_call_args, last_call_kwargs = push_calls[0]
528 assert last_call_kwargs['action'] == 'push'
528 assert last_call_kwargs['action'] == 'push'
529 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
529 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
530
530
531 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
531 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
532 pull_request = pr_util.create_pull_request(mergeable=False)
532 pull_request = pr_util.create_pull_request(mergeable=False)
533 pull_request_id = pull_request.pull_request_id
533 pull_request_id = pull_request.pull_request_id
534 pull_request = PullRequest.get(pull_request_id)
534 pull_request = PullRequest.get(pull_request_id)
535
535
536 response = self.app.post(
536 response = self.app.post(
537 url(controller='pullrequests',
537 url(controller='pullrequests',
538 action='merge',
538 action='merge',
539 repo_name=pull_request.target_repo.scm_instance().name,
539 repo_name=pull_request.target_repo.scm_instance().name,
540 pull_request_id=str(pull_request.pull_request_id)),
540 pull_request_id=str(pull_request.pull_request_id)),
541 params={'csrf_token': csrf_token}).follow()
541 params={'csrf_token': csrf_token}).follow()
542
542
543 assert response.status_int == 200
543 assert response.status_int == 200
544 assert 'Server-side pull request merging is disabled.' in response.body
544 response.mustcontain(
545 'Merge is not currently possible because of below failed checks.')
546 response.mustcontain('Server-side pull request merging is disabled.')
545
547
546 @pytest.mark.skip_backends('svn')
548 @pytest.mark.skip_backends('svn')
547 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
549 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
548 pull_request = pr_util.create_pull_request(mergeable=True)
550 pull_request = pr_util.create_pull_request(mergeable=True)
549 pull_request_id = pull_request.pull_request_id
551 pull_request_id = pull_request.pull_request_id
550 repo_name = pull_request.target_repo.scm_instance().name,
552 repo_name = pull_request.target_repo.scm_instance().name,
551
553
552 response = self.app.post(
554 response = self.app.post(
553 url(controller='pullrequests',
555 url(controller='pullrequests',
554 action='merge',
556 action='merge',
555 repo_name=str(repo_name[0]),
557 repo_name=str(repo_name[0]),
556 pull_request_id=str(pull_request_id)),
558 pull_request_id=str(pull_request_id)),
557 params={'csrf_token': csrf_token}).follow()
559 params={'csrf_token': csrf_token}).follow()
558
560
559 pull_request = PullRequest.get(pull_request_id)
561 assert response.status_int == 200
560
562
561 assert response.status_int == 200
563 response.mustcontain(
562 assert ' Reviewer approval is pending.' in response.body
564 'Merge is not currently possible because of below failed checks.')
565 response.mustcontain('Pull request reviewer approval is pending.')
563
566
564 def test_update_source_revision(self, backend, csrf_token):
567 def test_update_source_revision(self, backend, csrf_token):
565 commits = [
568 commits = [
566 {'message': 'ancestor'},
569 {'message': 'ancestor'},
567 {'message': 'change'},
570 {'message': 'change'},
568 {'message': 'change-2'},
571 {'message': 'change-2'},
569 ]
572 ]
570 commit_ids = backend.create_master_repo(commits)
573 commit_ids = backend.create_master_repo(commits)
571 target = backend.create_repo(heads=['ancestor'])
574 target = backend.create_repo(heads=['ancestor'])
572 source = backend.create_repo(heads=['change'])
575 source = backend.create_repo(heads=['change'])
573
576
574 # create pr from a in source to A in target
577 # create pr from a in source to A in target
575 pull_request = PullRequest()
578 pull_request = PullRequest()
576 pull_request.source_repo = source
579 pull_request.source_repo = source
577 # TODO: johbo: Make sure that we write the source ref this way!
580 # TODO: johbo: Make sure that we write the source ref this way!
578 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
581 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
579 branch=backend.default_branch_name, commit_id=commit_ids['change'])
582 branch=backend.default_branch_name, commit_id=commit_ids['change'])
580 pull_request.target_repo = target
583 pull_request.target_repo = target
581
584
582 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
585 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
583 branch=backend.default_branch_name,
586 branch=backend.default_branch_name,
584 commit_id=commit_ids['ancestor'])
587 commit_id=commit_ids['ancestor'])
585 pull_request.revisions = [commit_ids['change']]
588 pull_request.revisions = [commit_ids['change']]
586 pull_request.title = u"Test"
589 pull_request.title = u"Test"
587 pull_request.description = u"Description"
590 pull_request.description = u"Description"
588 pull_request.author = UserModel().get_by_username(
591 pull_request.author = UserModel().get_by_username(
589 TEST_USER_ADMIN_LOGIN)
592 TEST_USER_ADMIN_LOGIN)
590 Session().add(pull_request)
593 Session().add(pull_request)
591 Session().commit()
594 Session().commit()
592 pull_request_id = pull_request.pull_request_id
595 pull_request_id = pull_request.pull_request_id
593
596
594 # source has ancestor - change - change-2
597 # source has ancestor - change - change-2
595 backend.pull_heads(source, heads=['change-2'])
598 backend.pull_heads(source, heads=['change-2'])
596
599
597 # update PR
600 # update PR
598 self.app.post(
601 self.app.post(
599 url(controller='pullrequests', action='update',
602 url(controller='pullrequests', action='update',
600 repo_name=target.repo_name,
603 repo_name=target.repo_name,
601 pull_request_id=str(pull_request_id)),
604 pull_request_id=str(pull_request_id)),
602 params={'update_commits': 'true', '_method': 'put',
605 params={'update_commits': 'true', '_method': 'put',
603 'csrf_token': csrf_token})
606 'csrf_token': csrf_token})
604
607
605 # check that we have now both revisions
608 # check that we have now both revisions
606 pull_request = PullRequest.get(pull_request_id)
609 pull_request = PullRequest.get(pull_request_id)
607 assert pull_request.revisions == [
610 assert pull_request.revisions == [
608 commit_ids['change-2'], commit_ids['change']]
611 commit_ids['change-2'], commit_ids['change']]
609
612
610 # TODO: johbo: this should be a test on its own
613 # TODO: johbo: this should be a test on its own
611 response = self.app.get(url(
614 response = self.app.get(url(
612 controller='pullrequests', action='index',
615 controller='pullrequests', action='index',
613 repo_name=target.repo_name))
616 repo_name=target.repo_name))
614 assert response.status_int == 200
617 assert response.status_int == 200
615 assert 'Pull request updated to' in response.body
618 assert 'Pull request updated to' in response.body
616 assert 'with 1 added, 0 removed commits.' in response.body
619 assert 'with 1 added, 0 removed commits.' in response.body
617
620
618 def test_update_target_revision(self, backend, csrf_token):
621 def test_update_target_revision(self, backend, csrf_token):
619 commits = [
622 commits = [
620 {'message': 'ancestor'},
623 {'message': 'ancestor'},
621 {'message': 'change'},
624 {'message': 'change'},
622 {'message': 'ancestor-new', 'parents': ['ancestor']},
625 {'message': 'ancestor-new', 'parents': ['ancestor']},
623 {'message': 'change-rebased'},
626 {'message': 'change-rebased'},
624 ]
627 ]
625 commit_ids = backend.create_master_repo(commits)
628 commit_ids = backend.create_master_repo(commits)
626 target = backend.create_repo(heads=['ancestor'])
629 target = backend.create_repo(heads=['ancestor'])
627 source = backend.create_repo(heads=['change'])
630 source = backend.create_repo(heads=['change'])
628
631
629 # create pr from a in source to A in target
632 # create pr from a in source to A in target
630 pull_request = PullRequest()
633 pull_request = PullRequest()
631 pull_request.source_repo = source
634 pull_request.source_repo = source
632 # TODO: johbo: Make sure that we write the source ref this way!
635 # TODO: johbo: Make sure that we write the source ref this way!
633 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
636 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
634 branch=backend.default_branch_name, commit_id=commit_ids['change'])
637 branch=backend.default_branch_name, commit_id=commit_ids['change'])
635 pull_request.target_repo = target
638 pull_request.target_repo = target
636 # TODO: johbo: Target ref should be branch based, since tip can jump
639 # TODO: johbo: Target ref should be branch based, since tip can jump
637 # from branch to branch
640 # from branch to branch
638 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
641 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
639 branch=backend.default_branch_name,
642 branch=backend.default_branch_name,
640 commit_id=commit_ids['ancestor'])
643 commit_id=commit_ids['ancestor'])
641 pull_request.revisions = [commit_ids['change']]
644 pull_request.revisions = [commit_ids['change']]
642 pull_request.title = u"Test"
645 pull_request.title = u"Test"
643 pull_request.description = u"Description"
646 pull_request.description = u"Description"
644 pull_request.author = UserModel().get_by_username(
647 pull_request.author = UserModel().get_by_username(
645 TEST_USER_ADMIN_LOGIN)
648 TEST_USER_ADMIN_LOGIN)
646 Session().add(pull_request)
649 Session().add(pull_request)
647 Session().commit()
650 Session().commit()
648 pull_request_id = pull_request.pull_request_id
651 pull_request_id = pull_request.pull_request_id
649
652
650 # target has ancestor - ancestor-new
653 # target has ancestor - ancestor-new
651 # source has ancestor - ancestor-new - change-rebased
654 # source has ancestor - ancestor-new - change-rebased
652 backend.pull_heads(target, heads=['ancestor-new'])
655 backend.pull_heads(target, heads=['ancestor-new'])
653 backend.pull_heads(source, heads=['change-rebased'])
656 backend.pull_heads(source, heads=['change-rebased'])
654
657
655 # update PR
658 # update PR
656 self.app.post(
659 self.app.post(
657 url(controller='pullrequests', action='update',
660 url(controller='pullrequests', action='update',
658 repo_name=target.repo_name,
661 repo_name=target.repo_name,
659 pull_request_id=str(pull_request_id)),
662 pull_request_id=str(pull_request_id)),
660 params={'update_commits': 'true', '_method': 'put',
663 params={'update_commits': 'true', '_method': 'put',
661 'csrf_token': csrf_token},
664 'csrf_token': csrf_token},
662 status=200)
665 status=200)
663
666
664 # check that we have now both revisions
667 # check that we have now both revisions
665 pull_request = PullRequest.get(pull_request_id)
668 pull_request = PullRequest.get(pull_request_id)
666 assert pull_request.revisions == [commit_ids['change-rebased']]
669 assert pull_request.revisions == [commit_ids['change-rebased']]
667 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
670 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
668 branch=backend.default_branch_name,
671 branch=backend.default_branch_name,
669 commit_id=commit_ids['ancestor-new'])
672 commit_id=commit_ids['ancestor-new'])
670
673
671 # TODO: johbo: This should be a test on its own
674 # TODO: johbo: This should be a test on its own
672 response = self.app.get(url(
675 response = self.app.get(url(
673 controller='pullrequests', action='index',
676 controller='pullrequests', action='index',
674 repo_name=target.repo_name))
677 repo_name=target.repo_name))
675 assert response.status_int == 200
678 assert response.status_int == 200
676 assert 'Pull request updated to' in response.body
679 assert 'Pull request updated to' in response.body
677 assert 'with 1 added, 1 removed commits.' in response.body
680 assert 'with 1 added, 1 removed commits.' in response.body
678
681
679 def test_update_of_ancestor_reference(self, backend, csrf_token):
682 def test_update_of_ancestor_reference(self, backend, csrf_token):
680 commits = [
683 commits = [
681 {'message': 'ancestor'},
684 {'message': 'ancestor'},
682 {'message': 'change'},
685 {'message': 'change'},
683 {'message': 'change-2'},
686 {'message': 'change-2'},
684 {'message': 'ancestor-new', 'parents': ['ancestor']},
687 {'message': 'ancestor-new', 'parents': ['ancestor']},
685 {'message': 'change-rebased'},
688 {'message': 'change-rebased'},
686 ]
689 ]
687 commit_ids = backend.create_master_repo(commits)
690 commit_ids = backend.create_master_repo(commits)
688 target = backend.create_repo(heads=['ancestor'])
691 target = backend.create_repo(heads=['ancestor'])
689 source = backend.create_repo(heads=['change'])
692 source = backend.create_repo(heads=['change'])
690
693
691 # create pr from a in source to A in target
694 # create pr from a in source to A in target
692 pull_request = PullRequest()
695 pull_request = PullRequest()
693 pull_request.source_repo = source
696 pull_request.source_repo = source
694 # TODO: johbo: Make sure that we write the source ref this way!
697 # TODO: johbo: Make sure that we write the source ref this way!
695 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
698 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
696 branch=backend.default_branch_name,
699 branch=backend.default_branch_name,
697 commit_id=commit_ids['change'])
700 commit_id=commit_ids['change'])
698 pull_request.target_repo = target
701 pull_request.target_repo = target
699 # 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
700 # from branch to branch
703 # from branch to branch
701 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
702 branch=backend.default_branch_name,
705 branch=backend.default_branch_name,
703 commit_id=commit_ids['ancestor'])
706 commit_id=commit_ids['ancestor'])
704 pull_request.revisions = [commit_ids['change']]
707 pull_request.revisions = [commit_ids['change']]
705 pull_request.title = u"Test"
708 pull_request.title = u"Test"
706 pull_request.description = u"Description"
709 pull_request.description = u"Description"
707 pull_request.author = UserModel().get_by_username(
710 pull_request.author = UserModel().get_by_username(
708 TEST_USER_ADMIN_LOGIN)
711 TEST_USER_ADMIN_LOGIN)
709 Session().add(pull_request)
712 Session().add(pull_request)
710 Session().commit()
713 Session().commit()
711 pull_request_id = pull_request.pull_request_id
714 pull_request_id = pull_request.pull_request_id
712
715
713 # target has ancestor - ancestor-new
716 # target has ancestor - ancestor-new
714 # source has ancestor - ancestor-new - change-rebased
717 # source has ancestor - ancestor-new - change-rebased
715 backend.pull_heads(target, heads=['ancestor-new'])
718 backend.pull_heads(target, heads=['ancestor-new'])
716 backend.pull_heads(source, heads=['change-rebased'])
719 backend.pull_heads(source, heads=['change-rebased'])
717
720
718 # update PR
721 # update PR
719 self.app.post(
722 self.app.post(
720 url(controller='pullrequests', action='update',
723 url(controller='pullrequests', action='update',
721 repo_name=target.repo_name,
724 repo_name=target.repo_name,
722 pull_request_id=str(pull_request_id)),
725 pull_request_id=str(pull_request_id)),
723 params={'update_commits': 'true', '_method': 'put',
726 params={'update_commits': 'true', '_method': 'put',
724 'csrf_token': csrf_token},
727 'csrf_token': csrf_token},
725 status=200)
728 status=200)
726
729
727 # Expect the target reference to be updated correctly
730 # Expect the target reference to be updated correctly
728 pull_request = PullRequest.get(pull_request_id)
731 pull_request = PullRequest.get(pull_request_id)
729 assert pull_request.revisions == [commit_ids['change-rebased']]
732 assert pull_request.revisions == [commit_ids['change-rebased']]
730 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
733 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
731 branch=backend.default_branch_name,
734 branch=backend.default_branch_name,
732 commit_id=commit_ids['ancestor-new'])
735 commit_id=commit_ids['ancestor-new'])
733 assert pull_request.target_ref == expected_target_ref
736 assert pull_request.target_ref == expected_target_ref
734
737
735 def test_remove_pull_request_branch(self, backend_git, csrf_token):
738 def test_remove_pull_request_branch(self, backend_git, csrf_token):
736 branch_name = 'development'
739 branch_name = 'development'
737 commits = [
740 commits = [
738 {'message': 'initial-commit'},
741 {'message': 'initial-commit'},
739 {'message': 'old-feature'},
742 {'message': 'old-feature'},
740 {'message': 'new-feature', 'branch': branch_name},
743 {'message': 'new-feature', 'branch': branch_name},
741 ]
744 ]
742 repo = backend_git.create_repo(commits)
745 repo = backend_git.create_repo(commits)
743 commit_ids = backend_git.commit_ids
746 commit_ids = backend_git.commit_ids
744
747
745 pull_request = PullRequest()
748 pull_request = PullRequest()
746 pull_request.source_repo = repo
749 pull_request.source_repo = repo
747 pull_request.target_repo = repo
750 pull_request.target_repo = repo
748 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
751 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
749 branch=branch_name, commit_id=commit_ids['new-feature'])
752 branch=branch_name, commit_id=commit_ids['new-feature'])
750 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
753 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
751 branch=backend_git.default_branch_name,
754 branch=backend_git.default_branch_name,
752 commit_id=commit_ids['old-feature'])
755 commit_id=commit_ids['old-feature'])
753 pull_request.revisions = [commit_ids['new-feature']]
756 pull_request.revisions = [commit_ids['new-feature']]
754 pull_request.title = u"Test"
757 pull_request.title = u"Test"
755 pull_request.description = u"Description"
758 pull_request.description = u"Description"
756 pull_request.author = UserModel().get_by_username(
759 pull_request.author = UserModel().get_by_username(
757 TEST_USER_ADMIN_LOGIN)
760 TEST_USER_ADMIN_LOGIN)
758 Session().add(pull_request)
761 Session().add(pull_request)
759 Session().commit()
762 Session().commit()
760
763
761 vcs = repo.scm_instance()
764 vcs = repo.scm_instance()
762 vcs.remove_ref('refs/heads/{}'.format(branch_name))
765 vcs.remove_ref('refs/heads/{}'.format(branch_name))
763
766
764 response = self.app.get(url(
767 response = self.app.get(url(
765 controller='pullrequests', action='show',
768 controller='pullrequests', action='show',
766 repo_name=repo.repo_name,
769 repo_name=repo.repo_name,
767 pull_request_id=str(pull_request.pull_request_id)))
770 pull_request_id=str(pull_request.pull_request_id)))
768
771
769 assert response.status_int == 200
772 assert response.status_int == 200
770 assert_response = AssertResponse(response)
773 assert_response = AssertResponse(response)
771 assert_response.element_contains(
774 assert_response.element_contains(
772 '#changeset_compare_view_content .alert strong',
775 '#changeset_compare_view_content .alert strong',
773 'Missing commits')
776 'Missing commits')
774 assert_response.element_contains(
777 assert_response.element_contains(
775 '#changeset_compare_view_content .alert',
778 '#changeset_compare_view_content .alert',
776 'This pull request cannot be displayed, because one or more'
779 'This pull request cannot be displayed, because one or more'
777 ' commits no longer exist in the source repository.')
780 ' commits no longer exist in the source repository.')
778
781
779 def test_strip_commits_from_pull_request(
782 def test_strip_commits_from_pull_request(
780 self, backend, pr_util, csrf_token):
783 self, backend, pr_util, csrf_token):
781 commits = [
784 commits = [
782 {'message': 'initial-commit'},
785 {'message': 'initial-commit'},
783 {'message': 'old-feature'},
786 {'message': 'old-feature'},
784 {'message': 'new-feature', 'parents': ['initial-commit']},
787 {'message': 'new-feature', 'parents': ['initial-commit']},
785 ]
788 ]
786 pull_request = pr_util.create_pull_request(
789 pull_request = pr_util.create_pull_request(
787 commits, target_head='initial-commit', source_head='new-feature',
790 commits, target_head='initial-commit', source_head='new-feature',
788 revisions=['new-feature'])
791 revisions=['new-feature'])
789
792
790 vcs = pr_util.source_repository.scm_instance()
793 vcs = pr_util.source_repository.scm_instance()
791 if backend.alias == 'git':
794 if backend.alias == 'git':
792 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
795 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
793 else:
796 else:
794 vcs.strip(pr_util.commit_ids['new-feature'])
797 vcs.strip(pr_util.commit_ids['new-feature'])
795
798
796 response = self.app.get(url(
799 response = self.app.get(url(
797 controller='pullrequests', action='show',
800 controller='pullrequests', action='show',
798 repo_name=pr_util.target_repository.repo_name,
801 repo_name=pr_util.target_repository.repo_name,
799 pull_request_id=str(pull_request.pull_request_id)))
802 pull_request_id=str(pull_request.pull_request_id)))
800
803
801 assert response.status_int == 200
804 assert response.status_int == 200
802 assert_response = AssertResponse(response)
805 assert_response = AssertResponse(response)
803 assert_response.element_contains(
806 assert_response.element_contains(
804 '#changeset_compare_view_content .alert strong',
807 '#changeset_compare_view_content .alert strong',
805 'Missing commits')
808 'Missing commits')
806 assert_response.element_contains(
809 assert_response.element_contains(
807 '#changeset_compare_view_content .alert',
810 '#changeset_compare_view_content .alert',
808 'This pull request cannot be displayed, because one or more'
811 'This pull request cannot be displayed, because one or more'
809 ' commits no longer exist in the source repository.')
812 ' commits no longer exist in the source repository.')
810 assert_response.element_contains(
813 assert_response.element_contains(
811 '#update_commits',
814 '#update_commits',
812 'Update commits')
815 'Update commits')
813
816
814 def test_strip_commits_and_update(
817 def test_strip_commits_and_update(
815 self, backend, pr_util, csrf_token):
818 self, backend, pr_util, csrf_token):
816 commits = [
819 commits = [
817 {'message': 'initial-commit'},
820 {'message': 'initial-commit'},
818 {'message': 'old-feature'},
821 {'message': 'old-feature'},
819 {'message': 'new-feature', 'parents': ['old-feature']},
822 {'message': 'new-feature', 'parents': ['old-feature']},
820 ]
823 ]
821 pull_request = pr_util.create_pull_request(
824 pull_request = pr_util.create_pull_request(
822 commits, target_head='old-feature', source_head='new-feature',
825 commits, target_head='old-feature', source_head='new-feature',
823 revisions=['new-feature'], mergeable=True)
826 revisions=['new-feature'], mergeable=True)
824
827
825 vcs = pr_util.source_repository.scm_instance()
828 vcs = pr_util.source_repository.scm_instance()
826 if backend.alias == 'git':
829 if backend.alias == 'git':
827 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
830 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
828 else:
831 else:
829 vcs.strip(pr_util.commit_ids['new-feature'])
832 vcs.strip(pr_util.commit_ids['new-feature'])
830
833
831 response = self.app.post(
834 response = self.app.post(
832 url(controller='pullrequests', action='update',
835 url(controller='pullrequests', action='update',
833 repo_name=pull_request.target_repo.repo_name,
836 repo_name=pull_request.target_repo.repo_name,
834 pull_request_id=str(pull_request.pull_request_id)),
837 pull_request_id=str(pull_request.pull_request_id)),
835 params={'update_commits': 'true', '_method': 'put',
838 params={'update_commits': 'true', '_method': 'put',
836 'csrf_token': csrf_token})
839 'csrf_token': csrf_token})
837
840
838 assert response.status_int == 200
841 assert response.status_int == 200
839 assert response.body == 'true'
842 assert response.body == 'true'
840
843
841 # Make sure that after update, it won't raise 500 errors
844 # Make sure that after update, it won't raise 500 errors
842 response = self.app.get(url(
845 response = self.app.get(url(
843 controller='pullrequests', action='show',
846 controller='pullrequests', action='show',
844 repo_name=pr_util.target_repository.repo_name,
847 repo_name=pr_util.target_repository.repo_name,
845 pull_request_id=str(pull_request.pull_request_id)))
848 pull_request_id=str(pull_request.pull_request_id)))
846
849
847 assert response.status_int == 200
850 assert response.status_int == 200
848 assert_response = AssertResponse(response)
851 assert_response = AssertResponse(response)
849 assert_response.element_contains(
852 assert_response.element_contains(
850 '#changeset_compare_view_content .alert strong',
853 '#changeset_compare_view_content .alert strong',
851 'Missing commits')
854 'Missing commits')
852
855
853 def test_branch_is_a_link(self, pr_util):
856 def test_branch_is_a_link(self, pr_util):
854 pull_request = pr_util.create_pull_request()
857 pull_request = pr_util.create_pull_request()
855 pull_request.source_ref = 'branch:origin:1234567890abcdef'
858 pull_request.source_ref = 'branch:origin:1234567890abcdef'
856 pull_request.target_ref = 'branch:target:abcdef1234567890'
859 pull_request.target_ref = 'branch:target:abcdef1234567890'
857 Session().add(pull_request)
860 Session().add(pull_request)
858 Session().commit()
861 Session().commit()
859
862
860 response = self.app.get(url(
863 response = self.app.get(url(
861 controller='pullrequests', action='show',
864 controller='pullrequests', action='show',
862 repo_name=pull_request.target_repo.scm_instance().name,
865 repo_name=pull_request.target_repo.scm_instance().name,
863 pull_request_id=str(pull_request.pull_request_id)))
866 pull_request_id=str(pull_request.pull_request_id)))
864 assert response.status_int == 200
867 assert response.status_int == 200
865 assert_response = AssertResponse(response)
868 assert_response = AssertResponse(response)
866
869
867 origin = assert_response.get_element('.pr-origininfo .tag')
870 origin = assert_response.get_element('.pr-origininfo .tag')
868 origin_children = origin.getchildren()
871 origin_children = origin.getchildren()
869 assert len(origin_children) == 1
872 assert len(origin_children) == 1
870 target = assert_response.get_element('.pr-targetinfo .tag')
873 target = assert_response.get_element('.pr-targetinfo .tag')
871 target_children = target.getchildren()
874 target_children = target.getchildren()
872 assert len(target_children) == 1
875 assert len(target_children) == 1
873
876
874 expected_origin_link = url(
877 expected_origin_link = url(
875 'changelog_home',
878 'changelog_home',
876 repo_name=pull_request.source_repo.scm_instance().name,
879 repo_name=pull_request.source_repo.scm_instance().name,
877 branch='origin')
880 branch='origin')
878 expected_target_link = url(
881 expected_target_link = url(
879 'changelog_home',
882 'changelog_home',
880 repo_name=pull_request.target_repo.scm_instance().name,
883 repo_name=pull_request.target_repo.scm_instance().name,
881 branch='target')
884 branch='target')
882 assert origin_children[0].attrib['href'] == expected_origin_link
885 assert origin_children[0].attrib['href'] == expected_origin_link
883 assert origin_children[0].text == 'branch: origin'
886 assert origin_children[0].text == 'branch: origin'
884 assert target_children[0].attrib['href'] == expected_target_link
887 assert target_children[0].attrib['href'] == expected_target_link
885 assert target_children[0].text == 'branch: target'
888 assert target_children[0].text == 'branch: target'
886
889
887 def test_bookmark_is_not_a_link(self, pr_util):
890 def test_bookmark_is_not_a_link(self, pr_util):
888 pull_request = pr_util.create_pull_request()
891 pull_request = pr_util.create_pull_request()
889 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
892 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
890 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
893 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
891 Session().add(pull_request)
894 Session().add(pull_request)
892 Session().commit()
895 Session().commit()
893
896
894 response = self.app.get(url(
897 response = self.app.get(url(
895 controller='pullrequests', action='show',
898 controller='pullrequests', action='show',
896 repo_name=pull_request.target_repo.scm_instance().name,
899 repo_name=pull_request.target_repo.scm_instance().name,
897 pull_request_id=str(pull_request.pull_request_id)))
900 pull_request_id=str(pull_request.pull_request_id)))
898 assert response.status_int == 200
901 assert response.status_int == 200
899 assert_response = AssertResponse(response)
902 assert_response = AssertResponse(response)
900
903
901 origin = assert_response.get_element('.pr-origininfo .tag')
904 origin = assert_response.get_element('.pr-origininfo .tag')
902 assert origin.text.strip() == 'bookmark: origin'
905 assert origin.text.strip() == 'bookmark: origin'
903 assert origin.getchildren() == []
906 assert origin.getchildren() == []
904
907
905 target = assert_response.get_element('.pr-targetinfo .tag')
908 target = assert_response.get_element('.pr-targetinfo .tag')
906 assert target.text.strip() == 'bookmark: target'
909 assert target.text.strip() == 'bookmark: target'
907 assert target.getchildren() == []
910 assert target.getchildren() == []
908
911
909 def test_tag_is_not_a_link(self, pr_util):
912 def test_tag_is_not_a_link(self, pr_util):
910 pull_request = pr_util.create_pull_request()
913 pull_request = pr_util.create_pull_request()
911 pull_request.source_ref = 'tag:origin:1234567890abcdef'
914 pull_request.source_ref = 'tag:origin:1234567890abcdef'
912 pull_request.target_ref = 'tag:target:abcdef1234567890'
915 pull_request.target_ref = 'tag:target:abcdef1234567890'
913 Session().add(pull_request)
916 Session().add(pull_request)
914 Session().commit()
917 Session().commit()
915
918
916 response = self.app.get(url(
919 response = self.app.get(url(
917 controller='pullrequests', action='show',
920 controller='pullrequests', action='show',
918 repo_name=pull_request.target_repo.scm_instance().name,
921 repo_name=pull_request.target_repo.scm_instance().name,
919 pull_request_id=str(pull_request.pull_request_id)))
922 pull_request_id=str(pull_request.pull_request_id)))
920 assert response.status_int == 200
923 assert response.status_int == 200
921 assert_response = AssertResponse(response)
924 assert_response = AssertResponse(response)
922
925
923 origin = assert_response.get_element('.pr-origininfo .tag')
926 origin = assert_response.get_element('.pr-origininfo .tag')
924 assert origin.text.strip() == 'tag: origin'
927 assert origin.text.strip() == 'tag: origin'
925 assert origin.getchildren() == []
928 assert origin.getchildren() == []
926
929
927 target = assert_response.get_element('.pr-targetinfo .tag')
930 target = assert_response.get_element('.pr-targetinfo .tag')
928 assert target.text.strip() == 'tag: target'
931 assert target.text.strip() == 'tag: target'
929 assert target.getchildren() == []
932 assert target.getchildren() == []
930
933
931 def test_description_is_escaped_on_index_page(self, backend, pr_util):
934 def test_description_is_escaped_on_index_page(self, backend, pr_util):
932 xss_description = "<script>alert('Hi!')</script>"
935 xss_description = "<script>alert('Hi!')</script>"
933 pull_request = pr_util.create_pull_request(description=xss_description)
936 pull_request = pr_util.create_pull_request(description=xss_description)
934 response = self.app.get(url(
937 response = self.app.get(url(
935 controller='pullrequests', action='show_all',
938 controller='pullrequests', action='show_all',
936 repo_name=pull_request.target_repo.repo_name))
939 repo_name=pull_request.target_repo.repo_name))
937 response.mustcontain(
940 response.mustcontain(
938 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
941 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
939
942
940 @pytest.mark.parametrize('mergeable', [True, False])
943 @pytest.mark.parametrize('mergeable', [True, False])
941 def test_shadow_repository_link(
944 def test_shadow_repository_link(
942 self, mergeable, pr_util, http_host_stub):
945 self, mergeable, pr_util, http_host_stub):
943 """
946 """
944 Check that the pull request summary page displays a link to the shadow
947 Check that the pull request summary page displays a link to the shadow
945 repository if the pull request is mergeable. If it is not mergeable
948 repository if the pull request is mergeable. If it is not mergeable
946 the link should not be displayed.
949 the link should not be displayed.
947 """
950 """
948 pull_request = pr_util.create_pull_request(
951 pull_request = pr_util.create_pull_request(
949 mergeable=mergeable, enable_notifications=False)
952 mergeable=mergeable, enable_notifications=False)
950 target_repo = pull_request.target_repo.scm_instance()
953 target_repo = pull_request.target_repo.scm_instance()
951 pr_id = pull_request.pull_request_id
954 pr_id = pull_request.pull_request_id
952 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
955 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
953 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
956 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
954
957
955 response = self.app.get(url(
958 response = self.app.get(url(
956 controller='pullrequests', action='show',
959 controller='pullrequests', action='show',
957 repo_name=target_repo.name,
960 repo_name=target_repo.name,
958 pull_request_id=str(pr_id)))
961 pull_request_id=str(pr_id)))
959
962
960 assertr = AssertResponse(response)
963 assertr = AssertResponse(response)
961 if mergeable:
964 if mergeable:
962 assertr.element_value_contains(
965 assertr.element_value_contains(
963 'div.pr-mergeinfo input', shadow_url)
966 'div.pr-mergeinfo input', shadow_url)
964 assertr.element_value_contains(
967 assertr.element_value_contains(
965 'div.pr-mergeinfo input', 'pr-merge')
968 'div.pr-mergeinfo input', 'pr-merge')
966 else:
969 else:
967 assertr.no_element_exists('div.pr-mergeinfo')
970 assertr.no_element_exists('div.pr-mergeinfo')
968
971
969
972
970 @pytest.mark.usefixtures('app')
973 @pytest.mark.usefixtures('app')
971 @pytest.mark.backends("git", "hg")
974 @pytest.mark.backends("git", "hg")
972 class TestPullrequestsControllerDelete(object):
975 class TestPullrequestsControllerDelete(object):
973 def test_pull_request_delete_button_permissions_admin(
976 def test_pull_request_delete_button_permissions_admin(
974 self, autologin_user, user_admin, pr_util):
977 self, autologin_user, user_admin, pr_util):
975 pull_request = pr_util.create_pull_request(
978 pull_request = pr_util.create_pull_request(
976 author=user_admin.username, enable_notifications=False)
979 author=user_admin.username, enable_notifications=False)
977
980
978 response = self.app.get(url(
981 response = self.app.get(url(
979 controller='pullrequests', action='show',
982 controller='pullrequests', action='show',
980 repo_name=pull_request.target_repo.scm_instance().name,
983 repo_name=pull_request.target_repo.scm_instance().name,
981 pull_request_id=str(pull_request.pull_request_id)))
984 pull_request_id=str(pull_request.pull_request_id)))
982
985
983 response.mustcontain('id="delete_pullrequest"')
986 response.mustcontain('id="delete_pullrequest"')
984 response.mustcontain('Confirm to delete this pull request')
987 response.mustcontain('Confirm to delete this pull request')
985
988
986 def test_pull_request_delete_button_permissions_owner(
989 def test_pull_request_delete_button_permissions_owner(
987 self, autologin_regular_user, user_regular, pr_util):
990 self, autologin_regular_user, user_regular, pr_util):
988 pull_request = pr_util.create_pull_request(
991 pull_request = pr_util.create_pull_request(
989 author=user_regular.username, enable_notifications=False)
992 author=user_regular.username, enable_notifications=False)
990
993
991 response = self.app.get(url(
994 response = self.app.get(url(
992 controller='pullrequests', action='show',
995 controller='pullrequests', action='show',
993 repo_name=pull_request.target_repo.scm_instance().name,
996 repo_name=pull_request.target_repo.scm_instance().name,
994 pull_request_id=str(pull_request.pull_request_id)))
997 pull_request_id=str(pull_request.pull_request_id)))
995
998
996 response.mustcontain('id="delete_pullrequest"')
999 response.mustcontain('id="delete_pullrequest"')
997 response.mustcontain('Confirm to delete this pull request')
1000 response.mustcontain('Confirm to delete this pull request')
998
1001
999 def test_pull_request_delete_button_permissions_forbidden(
1002 def test_pull_request_delete_button_permissions_forbidden(
1000 self, autologin_regular_user, user_regular, user_admin, pr_util):
1003 self, autologin_regular_user, user_regular, user_admin, pr_util):
1001 pull_request = pr_util.create_pull_request(
1004 pull_request = pr_util.create_pull_request(
1002 author=user_admin.username, enable_notifications=False)
1005 author=user_admin.username, enable_notifications=False)
1003
1006
1004 response = self.app.get(url(
1007 response = self.app.get(url(
1005 controller='pullrequests', action='show',
1008 controller='pullrequests', action='show',
1006 repo_name=pull_request.target_repo.scm_instance().name,
1009 repo_name=pull_request.target_repo.scm_instance().name,
1007 pull_request_id=str(pull_request.pull_request_id)))
1010 pull_request_id=str(pull_request.pull_request_id)))
1008 response.mustcontain(no=['id="delete_pullrequest"'])
1011 response.mustcontain(no=['id="delete_pullrequest"'])
1009 response.mustcontain(no=['Confirm to delete this pull request'])
1012 response.mustcontain(no=['Confirm to delete this pull request'])
1010
1013
1011 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1014 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1012 self, autologin_regular_user, user_regular, user_admin, pr_util,
1015 self, autologin_regular_user, user_regular, user_admin, pr_util,
1013 user_util):
1016 user_util):
1014
1017
1015 pull_request = pr_util.create_pull_request(
1018 pull_request = pr_util.create_pull_request(
1016 author=user_admin.username, enable_notifications=False)
1019 author=user_admin.username, enable_notifications=False)
1017
1020
1018 user_util.grant_user_permission_to_repo(
1021 user_util.grant_user_permission_to_repo(
1019 pull_request.target_repo, user_regular,
1022 pull_request.target_repo, user_regular,
1020 'repository.write')
1023 'repository.write')
1021
1024
1022 response = self.app.get(url(
1025 response = self.app.get(url(
1023 controller='pullrequests', action='show',
1026 controller='pullrequests', action='show',
1024 repo_name=pull_request.target_repo.scm_instance().name,
1027 repo_name=pull_request.target_repo.scm_instance().name,
1025 pull_request_id=str(pull_request.pull_request_id)))
1028 pull_request_id=str(pull_request.pull_request_id)))
1026
1029
1027 response.mustcontain('id="open_edit_pullrequest"')
1030 response.mustcontain('id="open_edit_pullrequest"')
1028 response.mustcontain('id="delete_pullrequest"')
1031 response.mustcontain('id="delete_pullrequest"')
1029 response.mustcontain(no=['Confirm to delete this pull request'])
1032 response.mustcontain(no=['Confirm to delete this pull request'])
1030
1033
1031
1034
1032 def assert_pull_request_status(pull_request, expected_status):
1035 def assert_pull_request_status(pull_request, expected_status):
1033 status = ChangesetStatusModel().calculated_review_status(
1036 status = ChangesetStatusModel().calculated_review_status(
1034 pull_request=pull_request)
1037 pull_request=pull_request)
1035 assert status == expected_status
1038 assert status == expected_status
1036
1039
1037
1040
1038 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1041 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1039 @pytest.mark.usefixtures("autologin_user")
1042 @pytest.mark.usefixtures("autologin_user")
1040 def test_redirects_to_repo_summary_for_svn_repositories(
1043 def test_redirects_to_repo_summary_for_svn_repositories(
1041 backend_svn, app, action):
1044 backend_svn, app, action):
1042 denied_actions = ['show_all', 'index', 'create']
1045 denied_actions = ['show_all', 'index', 'create']
1043 for action in denied_actions:
1046 for action in denied_actions:
1044 response = app.get(url(
1047 response = app.get(url(
1045 controller='pullrequests', action=action,
1048 controller='pullrequests', action=action,
1046 repo_name=backend_svn.repo_name))
1049 repo_name=backend_svn.repo_name))
1047 assert response.status_int == 302
1050 assert response.status_int == 302
1048
1051
1049 # Not allowed, redirect to the summary
1052 # Not allowed, redirect to the summary
1050 redirected = response.follow()
1053 redirected = response.follow()
1051 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1054 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1052
1055
1053 # URL adds leading slash and path doesn't have it
1056 # URL adds leading slash and path doesn't have it
1054 assert redirected.req.path == summary_url
1057 assert redirected.req.path == summary_url
1055
1058
1056
1059
1057 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1060 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1058 # TODO: johbo: Global import not possible because models.forms blows up
1061 # TODO: johbo: Global import not possible because models.forms blows up
1059 from rhodecode.controllers.pullrequests import PullrequestsController
1062 from rhodecode.controllers.pullrequests import PullrequestsController
1060 controller = PullrequestsController()
1063 controller = PullrequestsController()
1061 patcher = mock.patch(
1064 patcher = mock.patch(
1062 'rhodecode.model.db.BaseModel.get', return_value=None)
1065 'rhodecode.model.db.BaseModel.get', return_value=None)
1063 with pytest.raises(HTTPNotFound), patcher:
1066 with pytest.raises(HTTPNotFound), patcher:
1064 controller._delete_comment(1)
1067 controller._delete_comment(1)
General Comments 0
You need to be logged in to leave comments. Login now