##// END OF EJS Templates
pull-requests: moved force refresh to update commits button....
marcink -
r4101:f857226e default
parent child Browse files
Show More
@@ -1,1217 +1,1217 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.tests import (
33 from rhodecode.tests import (
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35
35
36
36
37 def route_path(name, params=None, **kwargs):
37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib
39
39
40 base_url = {
40 base_url = {
41 'repo_changelog': '/{repo_name}/changelog',
41 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
42 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_commits': '/{repo_name}/commits',
43 'repo_commits': '/{repo_name}/commits',
44 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
44 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all': '/{repo_name}/pull-request',
47 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_new': '/{repo_name}/pull-request/new',
51 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_create': '/{repo_name}/pull-request/create',
52 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
56 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 }[name].format(**kwargs)
57 }[name].format(**kwargs)
58
58
59 if params:
59 if params:
60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
61 return base_url
61 return base_url
62
62
63
63
64 @pytest.mark.usefixtures('app', 'autologin_user')
64 @pytest.mark.usefixtures('app', 'autologin_user')
65 @pytest.mark.backends("git", "hg")
65 @pytest.mark.backends("git", "hg")
66 class TestPullrequestsView(object):
66 class TestPullrequestsView(object):
67
67
68 def test_index(self, backend):
68 def test_index(self, backend):
69 self.app.get(route_path(
69 self.app.get(route_path(
70 'pullrequest_new',
70 'pullrequest_new',
71 repo_name=backend.repo_name))
71 repo_name=backend.repo_name))
72
72
73 def test_option_menu_create_pull_request_exists(self, backend):
73 def test_option_menu_create_pull_request_exists(self, backend):
74 repo_name = backend.repo_name
74 repo_name = backend.repo_name
75 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
76
76
77 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
78 'pullrequest_new', repo_name=repo_name)
78 'pullrequest_new', repo_name=repo_name)
79 response.mustcontain(create_pr_link)
79 response.mustcontain(create_pr_link)
80
80
81 def test_create_pr_form_with_raw_commit_id(self, backend):
81 def test_create_pr_form_with_raw_commit_id(self, backend):
82 repo = backend.repo
82 repo = backend.repo
83
83
84 self.app.get(
84 self.app.get(
85 route_path('pullrequest_new', repo_name=repo.repo_name,
85 route_path('pullrequest_new', repo_name=repo.repo_name,
86 commit=repo.get_commit().raw_id),
86 commit=repo.get_commit().raw_id),
87 status=200)
87 status=200)
88
88
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 @pytest.mark.parametrize('range_diff', ["0", "1"])
90 @pytest.mark.parametrize('range_diff', ["0", "1"])
91 def test_show(self, pr_util, pr_merge_enabled, range_diff):
91 def test_show(self, pr_util, pr_merge_enabled, range_diff):
92 pull_request = pr_util.create_pull_request(
92 pull_request = pr_util.create_pull_request(
93 mergeable=pr_merge_enabled, enable_notifications=False)
93 mergeable=pr_merge_enabled, enable_notifications=False)
94
94
95 response = self.app.get(route_path(
95 response = self.app.get(route_path(
96 'pullrequest_show',
96 'pullrequest_show',
97 repo_name=pull_request.target_repo.scm_instance().name,
97 repo_name=pull_request.target_repo.scm_instance().name,
98 pull_request_id=pull_request.pull_request_id,
98 pull_request_id=pull_request.pull_request_id,
99 params={'range-diff': range_diff}))
99 params={'range-diff': range_diff}))
100
100
101 for commit_id in pull_request.revisions:
101 for commit_id in pull_request.revisions:
102 response.mustcontain(commit_id)
102 response.mustcontain(commit_id)
103
103
104 assert pull_request.target_ref_parts.type in response
104 assert pull_request.target_ref_parts.type in response
105 assert pull_request.target_ref_parts.name in response
105 assert pull_request.target_ref_parts.name in response
106 target_clone_url = pull_request.target_repo.clone_url()
106 target_clone_url = pull_request.target_repo.clone_url()
107 assert target_clone_url in response
107 assert target_clone_url in response
108
108
109 assert 'class="pull-request-merge"' in response
109 assert 'class="pull-request-merge"' in response
110 if pr_merge_enabled:
110 if pr_merge_enabled:
111 response.mustcontain('Pull request reviewer approval is pending')
111 response.mustcontain('Pull request reviewer approval is pending')
112 else:
112 else:
113 response.mustcontain('Server-side pull request merging is disabled.')
113 response.mustcontain('Server-side pull request merging is disabled.')
114
114
115 if range_diff == "1":
115 if range_diff == "1":
116 response.mustcontain('Turn off: Show the diff as commit range')
116 response.mustcontain('Turn off: Show the diff as commit range')
117
117
118 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
118 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
119 # Logout
119 # Logout
120 response = self.app.post(
120 response = self.app.post(
121 h.route_path('logout'),
121 h.route_path('logout'),
122 params={'csrf_token': csrf_token})
122 params={'csrf_token': csrf_token})
123 # Login as regular user
123 # Login as regular user
124 response = self.app.post(h.route_path('login'),
124 response = self.app.post(h.route_path('login'),
125 {'username': TEST_USER_REGULAR_LOGIN,
125 {'username': TEST_USER_REGULAR_LOGIN,
126 'password': 'test12'})
126 'password': 'test12'})
127
127
128 pull_request = pr_util.create_pull_request(
128 pull_request = pr_util.create_pull_request(
129 author=TEST_USER_REGULAR_LOGIN)
129 author=TEST_USER_REGULAR_LOGIN)
130
130
131 response = self.app.get(route_path(
131 response = self.app.get(route_path(
132 'pullrequest_show',
132 'pullrequest_show',
133 repo_name=pull_request.target_repo.scm_instance().name,
133 repo_name=pull_request.target_repo.scm_instance().name,
134 pull_request_id=pull_request.pull_request_id))
134 pull_request_id=pull_request.pull_request_id))
135
135
136 response.mustcontain('Server-side pull request merging is disabled.')
136 response.mustcontain('Server-side pull request merging is disabled.')
137
137
138 assert_response = response.assert_response()
138 assert_response = response.assert_response()
139 # for regular user without a merge permissions, we don't see it
139 # for regular user without a merge permissions, we don't see it
140 assert_response.no_element_exists('#close-pull-request-action')
140 assert_response.no_element_exists('#close-pull-request-action')
141
141
142 user_util.grant_user_permission_to_repo(
142 user_util.grant_user_permission_to_repo(
143 pull_request.target_repo,
143 pull_request.target_repo,
144 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
144 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
145 'repository.write')
145 'repository.write')
146 response = self.app.get(route_path(
146 response = self.app.get(route_path(
147 'pullrequest_show',
147 'pullrequest_show',
148 repo_name=pull_request.target_repo.scm_instance().name,
148 repo_name=pull_request.target_repo.scm_instance().name,
149 pull_request_id=pull_request.pull_request_id))
149 pull_request_id=pull_request.pull_request_id))
150
150
151 response.mustcontain('Server-side pull request merging is disabled.')
151 response.mustcontain('Server-side pull request merging is disabled.')
152
152
153 assert_response = response.assert_response()
153 assert_response = response.assert_response()
154 # now regular user has a merge permissions, we have CLOSE button
154 # now regular user has a merge permissions, we have CLOSE button
155 assert_response.one_element_exists('#close-pull-request-action')
155 assert_response.one_element_exists('#close-pull-request-action')
156
156
157 def test_show_invalid_commit_id(self, pr_util):
157 def test_show_invalid_commit_id(self, pr_util):
158 # Simulating invalid revisions which will cause a lookup error
158 # Simulating invalid revisions which will cause a lookup error
159 pull_request = pr_util.create_pull_request()
159 pull_request = pr_util.create_pull_request()
160 pull_request.revisions = ['invalid']
160 pull_request.revisions = ['invalid']
161 Session().add(pull_request)
161 Session().add(pull_request)
162 Session().commit()
162 Session().commit()
163
163
164 response = self.app.get(route_path(
164 response = self.app.get(route_path(
165 'pullrequest_show',
165 'pullrequest_show',
166 repo_name=pull_request.target_repo.scm_instance().name,
166 repo_name=pull_request.target_repo.scm_instance().name,
167 pull_request_id=pull_request.pull_request_id))
167 pull_request_id=pull_request.pull_request_id))
168
168
169 for commit_id in pull_request.revisions:
169 for commit_id in pull_request.revisions:
170 response.mustcontain(commit_id)
170 response.mustcontain(commit_id)
171
171
172 def test_show_invalid_source_reference(self, pr_util):
172 def test_show_invalid_source_reference(self, pr_util):
173 pull_request = pr_util.create_pull_request()
173 pull_request = pr_util.create_pull_request()
174 pull_request.source_ref = 'branch:b:invalid'
174 pull_request.source_ref = 'branch:b:invalid'
175 Session().add(pull_request)
175 Session().add(pull_request)
176 Session().commit()
176 Session().commit()
177
177
178 self.app.get(route_path(
178 self.app.get(route_path(
179 'pullrequest_show',
179 'pullrequest_show',
180 repo_name=pull_request.target_repo.scm_instance().name,
180 repo_name=pull_request.target_repo.scm_instance().name,
181 pull_request_id=pull_request.pull_request_id))
181 pull_request_id=pull_request.pull_request_id))
182
182
183 def test_edit_title_description(self, pr_util, csrf_token):
183 def test_edit_title_description(self, pr_util, csrf_token):
184 pull_request = pr_util.create_pull_request()
184 pull_request = pr_util.create_pull_request()
185 pull_request_id = pull_request.pull_request_id
185 pull_request_id = pull_request.pull_request_id
186
186
187 response = self.app.post(
187 response = self.app.post(
188 route_path('pullrequest_update',
188 route_path('pullrequest_update',
189 repo_name=pull_request.target_repo.repo_name,
189 repo_name=pull_request.target_repo.repo_name,
190 pull_request_id=pull_request_id),
190 pull_request_id=pull_request_id),
191 params={
191 params={
192 'edit_pull_request': 'true',
192 'edit_pull_request': 'true',
193 'title': 'New title',
193 'title': 'New title',
194 'description': 'New description',
194 'description': 'New description',
195 'csrf_token': csrf_token})
195 'csrf_token': csrf_token})
196
196
197 assert_session_flash(
197 assert_session_flash(
198 response, u'Pull request title & description updated.',
198 response, u'Pull request title & description updated.',
199 category='success')
199 category='success')
200
200
201 pull_request = PullRequest.get(pull_request_id)
201 pull_request = PullRequest.get(pull_request_id)
202 assert pull_request.title == 'New title'
202 assert pull_request.title == 'New title'
203 assert pull_request.description == 'New description'
203 assert pull_request.description == 'New description'
204
204
205 def test_edit_title_description_closed(self, pr_util, csrf_token):
205 def test_edit_title_description_closed(self, pr_util, csrf_token):
206 pull_request = pr_util.create_pull_request()
206 pull_request = pr_util.create_pull_request()
207 pull_request_id = pull_request.pull_request_id
207 pull_request_id = pull_request.pull_request_id
208 repo_name = pull_request.target_repo.repo_name
208 repo_name = pull_request.target_repo.repo_name
209 pr_util.close()
209 pr_util.close()
210
210
211 response = self.app.post(
211 response = self.app.post(
212 route_path('pullrequest_update',
212 route_path('pullrequest_update',
213 repo_name=repo_name, pull_request_id=pull_request_id),
213 repo_name=repo_name, pull_request_id=pull_request_id),
214 params={
214 params={
215 'edit_pull_request': 'true',
215 'edit_pull_request': 'true',
216 'title': 'New title',
216 'title': 'New title',
217 'description': 'New description',
217 'description': 'New description',
218 'csrf_token': csrf_token}, status=200)
218 'csrf_token': csrf_token}, status=200)
219 assert_session_flash(
219 assert_session_flash(
220 response, u'Cannot update closed pull requests.',
220 response, u'Cannot update closed pull requests.',
221 category='error')
221 category='error')
222
222
223 def test_update_invalid_source_reference(self, pr_util, csrf_token):
223 def test_update_invalid_source_reference(self, pr_util, csrf_token):
224 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
224 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
225
225
226 pull_request = pr_util.create_pull_request()
226 pull_request = pr_util.create_pull_request()
227 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
227 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
228 Session().add(pull_request)
228 Session().add(pull_request)
229 Session().commit()
229 Session().commit()
230
230
231 pull_request_id = pull_request.pull_request_id
231 pull_request_id = pull_request.pull_request_id
232
232
233 response = self.app.post(
233 response = self.app.post(
234 route_path('pullrequest_update',
234 route_path('pullrequest_update',
235 repo_name=pull_request.target_repo.repo_name,
235 repo_name=pull_request.target_repo.repo_name,
236 pull_request_id=pull_request_id),
236 pull_request_id=pull_request_id),
237 params={'update_commits': 'true', 'csrf_token': csrf_token})
237 params={'update_commits': 'true', 'csrf_token': csrf_token})
238
238
239 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
239 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 UpdateFailureReason.MISSING_SOURCE_REF])
240 UpdateFailureReason.MISSING_SOURCE_REF])
241 assert_session_flash(response, expected_msg, category='error')
241 assert_session_flash(response, expected_msg, category='error')
242
242
243 def test_missing_target_reference(self, pr_util, csrf_token):
243 def test_missing_target_reference(self, pr_util, csrf_token):
244 from rhodecode.lib.vcs.backends.base import MergeFailureReason
244 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 pull_request = pr_util.create_pull_request(
245 pull_request = pr_util.create_pull_request(
246 approved=True, mergeable=True)
246 approved=True, mergeable=True)
247 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
247 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
248 pull_request.target_ref = unicode_reference
248 pull_request.target_ref = unicode_reference
249 Session().add(pull_request)
249 Session().add(pull_request)
250 Session().commit()
250 Session().commit()
251
251
252 pull_request_id = pull_request.pull_request_id
252 pull_request_id = pull_request.pull_request_id
253 pull_request_url = route_path(
253 pull_request_url = route_path(
254 'pullrequest_show',
254 'pullrequest_show',
255 repo_name=pull_request.target_repo.repo_name,
255 repo_name=pull_request.target_repo.repo_name,
256 pull_request_id=pull_request_id)
256 pull_request_id=pull_request_id)
257
257
258 response = self.app.get(pull_request_url)
258 response = self.app.get(pull_request_url)
259 target_ref_id = 'invalid-branch'
259 target_ref_id = 'invalid-branch'
260 merge_resp = MergeResponse(
260 merge_resp = MergeResponse(
261 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
261 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
262 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
262 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
263 response.assert_response().element_contains(
263 response.assert_response().element_contains(
264 'div[data-role="merge-message"]', merge_resp.merge_status_message)
264 'div[data-role="merge-message"]', merge_resp.merge_status_message)
265
265
266 def test_comment_and_close_pull_request_custom_message_approved(
266 def test_comment_and_close_pull_request_custom_message_approved(
267 self, pr_util, csrf_token, xhr_header):
267 self, pr_util, csrf_token, xhr_header):
268
268
269 pull_request = pr_util.create_pull_request(approved=True)
269 pull_request = pr_util.create_pull_request(approved=True)
270 pull_request_id = pull_request.pull_request_id
270 pull_request_id = pull_request.pull_request_id
271 author = pull_request.user_id
271 author = pull_request.user_id
272 repo = pull_request.target_repo.repo_id
272 repo = pull_request.target_repo.repo_id
273
273
274 self.app.post(
274 self.app.post(
275 route_path('pullrequest_comment_create',
275 route_path('pullrequest_comment_create',
276 repo_name=pull_request.target_repo.scm_instance().name,
276 repo_name=pull_request.target_repo.scm_instance().name,
277 pull_request_id=pull_request_id),
277 pull_request_id=pull_request_id),
278 params={
278 params={
279 'close_pull_request': '1',
279 'close_pull_request': '1',
280 'text': 'Closing a PR',
280 'text': 'Closing a PR',
281 'csrf_token': csrf_token},
281 'csrf_token': csrf_token},
282 extra_environ=xhr_header,)
282 extra_environ=xhr_header,)
283
283
284 journal = UserLog.query()\
284 journal = UserLog.query()\
285 .filter(UserLog.user_id == author)\
285 .filter(UserLog.user_id == author)\
286 .filter(UserLog.repository_id == repo) \
286 .filter(UserLog.repository_id == repo) \
287 .order_by(UserLog.user_log_id.asc()) \
287 .order_by(UserLog.user_log_id.asc()) \
288 .all()
288 .all()
289 assert journal[-1].action == 'repo.pull_request.close'
289 assert journal[-1].action == 'repo.pull_request.close'
290
290
291 pull_request = PullRequest.get(pull_request_id)
291 pull_request = PullRequest.get(pull_request_id)
292 assert pull_request.is_closed()
292 assert pull_request.is_closed()
293
293
294 status = ChangesetStatusModel().get_status(
294 status = ChangesetStatusModel().get_status(
295 pull_request.source_repo, pull_request=pull_request)
295 pull_request.source_repo, pull_request=pull_request)
296 assert status == ChangesetStatus.STATUS_APPROVED
296 assert status == ChangesetStatus.STATUS_APPROVED
297 comments = ChangesetComment().query() \
297 comments = ChangesetComment().query() \
298 .filter(ChangesetComment.pull_request == pull_request) \
298 .filter(ChangesetComment.pull_request == pull_request) \
299 .order_by(ChangesetComment.comment_id.asc())\
299 .order_by(ChangesetComment.comment_id.asc())\
300 .all()
300 .all()
301 assert comments[-1].text == 'Closing a PR'
301 assert comments[-1].text == 'Closing a PR'
302
302
303 def test_comment_force_close_pull_request_rejected(
303 def test_comment_force_close_pull_request_rejected(
304 self, pr_util, csrf_token, xhr_header):
304 self, pr_util, csrf_token, xhr_header):
305 pull_request = pr_util.create_pull_request()
305 pull_request = pr_util.create_pull_request()
306 pull_request_id = pull_request.pull_request_id
306 pull_request_id = pull_request.pull_request_id
307 PullRequestModel().update_reviewers(
307 PullRequestModel().update_reviewers(
308 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
308 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
309 pull_request.author)
309 pull_request.author)
310 author = pull_request.user_id
310 author = pull_request.user_id
311 repo = pull_request.target_repo.repo_id
311 repo = pull_request.target_repo.repo_id
312
312
313 self.app.post(
313 self.app.post(
314 route_path('pullrequest_comment_create',
314 route_path('pullrequest_comment_create',
315 repo_name=pull_request.target_repo.scm_instance().name,
315 repo_name=pull_request.target_repo.scm_instance().name,
316 pull_request_id=pull_request_id),
316 pull_request_id=pull_request_id),
317 params={
317 params={
318 'close_pull_request': '1',
318 'close_pull_request': '1',
319 'csrf_token': csrf_token},
319 'csrf_token': csrf_token},
320 extra_environ=xhr_header)
320 extra_environ=xhr_header)
321
321
322 pull_request = PullRequest.get(pull_request_id)
322 pull_request = PullRequest.get(pull_request_id)
323
323
324 journal = UserLog.query()\
324 journal = UserLog.query()\
325 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
325 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
326 .order_by(UserLog.user_log_id.asc()) \
326 .order_by(UserLog.user_log_id.asc()) \
327 .all()
327 .all()
328 assert journal[-1].action == 'repo.pull_request.close'
328 assert journal[-1].action == 'repo.pull_request.close'
329
329
330 # check only the latest status, not the review status
330 # check only the latest status, not the review status
331 status = ChangesetStatusModel().get_status(
331 status = ChangesetStatusModel().get_status(
332 pull_request.source_repo, pull_request=pull_request)
332 pull_request.source_repo, pull_request=pull_request)
333 assert status == ChangesetStatus.STATUS_REJECTED
333 assert status == ChangesetStatus.STATUS_REJECTED
334
334
335 def test_comment_and_close_pull_request(
335 def test_comment_and_close_pull_request(
336 self, pr_util, csrf_token, xhr_header):
336 self, pr_util, csrf_token, xhr_header):
337 pull_request = pr_util.create_pull_request()
337 pull_request = pr_util.create_pull_request()
338 pull_request_id = pull_request.pull_request_id
338 pull_request_id = pull_request.pull_request_id
339
339
340 response = self.app.post(
340 response = self.app.post(
341 route_path('pullrequest_comment_create',
341 route_path('pullrequest_comment_create',
342 repo_name=pull_request.target_repo.scm_instance().name,
342 repo_name=pull_request.target_repo.scm_instance().name,
343 pull_request_id=pull_request.pull_request_id),
343 pull_request_id=pull_request.pull_request_id),
344 params={
344 params={
345 'close_pull_request': 'true',
345 'close_pull_request': 'true',
346 'csrf_token': csrf_token},
346 'csrf_token': csrf_token},
347 extra_environ=xhr_header)
347 extra_environ=xhr_header)
348
348
349 assert response.json
349 assert response.json
350
350
351 pull_request = PullRequest.get(pull_request_id)
351 pull_request = PullRequest.get(pull_request_id)
352 assert pull_request.is_closed()
352 assert pull_request.is_closed()
353
353
354 # check only the latest status, not the review status
354 # check only the latest status, not the review status
355 status = ChangesetStatusModel().get_status(
355 status = ChangesetStatusModel().get_status(
356 pull_request.source_repo, pull_request=pull_request)
356 pull_request.source_repo, pull_request=pull_request)
357 assert status == ChangesetStatus.STATUS_REJECTED
357 assert status == ChangesetStatus.STATUS_REJECTED
358
358
359 def test_create_pull_request(self, backend, csrf_token):
359 def test_create_pull_request(self, backend, csrf_token):
360 commits = [
360 commits = [
361 {'message': 'ancestor'},
361 {'message': 'ancestor'},
362 {'message': 'change'},
362 {'message': 'change'},
363 {'message': 'change2'},
363 {'message': 'change2'},
364 ]
364 ]
365 commit_ids = backend.create_master_repo(commits)
365 commit_ids = backend.create_master_repo(commits)
366 target = backend.create_repo(heads=['ancestor'])
366 target = backend.create_repo(heads=['ancestor'])
367 source = backend.create_repo(heads=['change2'])
367 source = backend.create_repo(heads=['change2'])
368
368
369 response = self.app.post(
369 response = self.app.post(
370 route_path('pullrequest_create', repo_name=source.repo_name),
370 route_path('pullrequest_create', repo_name=source.repo_name),
371 [
371 [
372 ('source_repo', source.repo_name),
372 ('source_repo', source.repo_name),
373 ('source_ref', 'branch:default:' + commit_ids['change2']),
373 ('source_ref', 'branch:default:' + commit_ids['change2']),
374 ('target_repo', target.repo_name),
374 ('target_repo', target.repo_name),
375 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
375 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
376 ('common_ancestor', commit_ids['ancestor']),
376 ('common_ancestor', commit_ids['ancestor']),
377 ('pullrequest_title', 'Title'),
377 ('pullrequest_title', 'Title'),
378 ('pullrequest_desc', 'Description'),
378 ('pullrequest_desc', 'Description'),
379 ('description_renderer', 'markdown'),
379 ('description_renderer', 'markdown'),
380 ('__start__', 'review_members:sequence'),
380 ('__start__', 'review_members:sequence'),
381 ('__start__', 'reviewer:mapping'),
381 ('__start__', 'reviewer:mapping'),
382 ('user_id', '1'),
382 ('user_id', '1'),
383 ('__start__', 'reasons:sequence'),
383 ('__start__', 'reasons:sequence'),
384 ('reason', 'Some reason'),
384 ('reason', 'Some reason'),
385 ('__end__', 'reasons:sequence'),
385 ('__end__', 'reasons:sequence'),
386 ('__start__', 'rules:sequence'),
386 ('__start__', 'rules:sequence'),
387 ('__end__', 'rules:sequence'),
387 ('__end__', 'rules:sequence'),
388 ('mandatory', 'False'),
388 ('mandatory', 'False'),
389 ('__end__', 'reviewer:mapping'),
389 ('__end__', 'reviewer:mapping'),
390 ('__end__', 'review_members:sequence'),
390 ('__end__', 'review_members:sequence'),
391 ('__start__', 'revisions:sequence'),
391 ('__start__', 'revisions:sequence'),
392 ('revisions', commit_ids['change']),
392 ('revisions', commit_ids['change']),
393 ('revisions', commit_ids['change2']),
393 ('revisions', commit_ids['change2']),
394 ('__end__', 'revisions:sequence'),
394 ('__end__', 'revisions:sequence'),
395 ('user', ''),
395 ('user', ''),
396 ('csrf_token', csrf_token),
396 ('csrf_token', csrf_token),
397 ],
397 ],
398 status=302)
398 status=302)
399
399
400 location = response.headers['Location']
400 location = response.headers['Location']
401 pull_request_id = location.rsplit('/', 1)[1]
401 pull_request_id = location.rsplit('/', 1)[1]
402 assert pull_request_id != 'new'
402 assert pull_request_id != 'new'
403 pull_request = PullRequest.get(int(pull_request_id))
403 pull_request = PullRequest.get(int(pull_request_id))
404
404
405 # check that we have now both revisions
405 # check that we have now both revisions
406 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
406 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
407 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
407 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
408 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
408 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
409 assert pull_request.target_ref == expected_target_ref
409 assert pull_request.target_ref == expected_target_ref
410
410
411 def test_reviewer_notifications(self, backend, csrf_token):
411 def test_reviewer_notifications(self, backend, csrf_token):
412 # We have to use the app.post for this test so it will create the
412 # We have to use the app.post for this test so it will create the
413 # notifications properly with the new PR
413 # notifications properly with the new PR
414 commits = [
414 commits = [
415 {'message': 'ancestor',
415 {'message': 'ancestor',
416 'added': [FileNode('file_A', content='content_of_ancestor')]},
416 'added': [FileNode('file_A', content='content_of_ancestor')]},
417 {'message': 'change',
417 {'message': 'change',
418 'added': [FileNode('file_a', content='content_of_change')]},
418 'added': [FileNode('file_a', content='content_of_change')]},
419 {'message': 'change-child'},
419 {'message': 'change-child'},
420 {'message': 'ancestor-child', 'parents': ['ancestor'],
420 {'message': 'ancestor-child', 'parents': ['ancestor'],
421 'added': [
421 'added': [
422 FileNode('file_B', content='content_of_ancestor_child')]},
422 FileNode('file_B', content='content_of_ancestor_child')]},
423 {'message': 'ancestor-child-2'},
423 {'message': 'ancestor-child-2'},
424 ]
424 ]
425 commit_ids = backend.create_master_repo(commits)
425 commit_ids = backend.create_master_repo(commits)
426 target = backend.create_repo(heads=['ancestor-child'])
426 target = backend.create_repo(heads=['ancestor-child'])
427 source = backend.create_repo(heads=['change'])
427 source = backend.create_repo(heads=['change'])
428
428
429 response = self.app.post(
429 response = self.app.post(
430 route_path('pullrequest_create', repo_name=source.repo_name),
430 route_path('pullrequest_create', repo_name=source.repo_name),
431 [
431 [
432 ('source_repo', source.repo_name),
432 ('source_repo', source.repo_name),
433 ('source_ref', 'branch:default:' + commit_ids['change']),
433 ('source_ref', 'branch:default:' + commit_ids['change']),
434 ('target_repo', target.repo_name),
434 ('target_repo', target.repo_name),
435 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
435 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
436 ('common_ancestor', commit_ids['ancestor']),
436 ('common_ancestor', commit_ids['ancestor']),
437 ('pullrequest_title', 'Title'),
437 ('pullrequest_title', 'Title'),
438 ('pullrequest_desc', 'Description'),
438 ('pullrequest_desc', 'Description'),
439 ('description_renderer', 'markdown'),
439 ('description_renderer', 'markdown'),
440 ('__start__', 'review_members:sequence'),
440 ('__start__', 'review_members:sequence'),
441 ('__start__', 'reviewer:mapping'),
441 ('__start__', 'reviewer:mapping'),
442 ('user_id', '2'),
442 ('user_id', '2'),
443 ('__start__', 'reasons:sequence'),
443 ('__start__', 'reasons:sequence'),
444 ('reason', 'Some reason'),
444 ('reason', 'Some reason'),
445 ('__end__', 'reasons:sequence'),
445 ('__end__', 'reasons:sequence'),
446 ('__start__', 'rules:sequence'),
446 ('__start__', 'rules:sequence'),
447 ('__end__', 'rules:sequence'),
447 ('__end__', 'rules:sequence'),
448 ('mandatory', 'False'),
448 ('mandatory', 'False'),
449 ('__end__', 'reviewer:mapping'),
449 ('__end__', 'reviewer:mapping'),
450 ('__end__', 'review_members:sequence'),
450 ('__end__', 'review_members:sequence'),
451 ('__start__', 'revisions:sequence'),
451 ('__start__', 'revisions:sequence'),
452 ('revisions', commit_ids['change']),
452 ('revisions', commit_ids['change']),
453 ('__end__', 'revisions:sequence'),
453 ('__end__', 'revisions:sequence'),
454 ('user', ''),
454 ('user', ''),
455 ('csrf_token', csrf_token),
455 ('csrf_token', csrf_token),
456 ],
456 ],
457 status=302)
457 status=302)
458
458
459 location = response.headers['Location']
459 location = response.headers['Location']
460
460
461 pull_request_id = location.rsplit('/', 1)[1]
461 pull_request_id = location.rsplit('/', 1)[1]
462 assert pull_request_id != 'new'
462 assert pull_request_id != 'new'
463 pull_request = PullRequest.get(int(pull_request_id))
463 pull_request = PullRequest.get(int(pull_request_id))
464
464
465 # Check that a notification was made
465 # Check that a notification was made
466 notifications = Notification.query()\
466 notifications = Notification.query()\
467 .filter(Notification.created_by == pull_request.author.user_id,
467 .filter(Notification.created_by == pull_request.author.user_id,
468 Notification.type_ == Notification.TYPE_PULL_REQUEST,
468 Notification.type_ == Notification.TYPE_PULL_REQUEST,
469 Notification.subject.contains(
469 Notification.subject.contains(
470 "requested a pull request review. !%s" % pull_request_id))
470 "requested a pull request review. !%s" % pull_request_id))
471 assert len(notifications.all()) == 1
471 assert len(notifications.all()) == 1
472
472
473 # Change reviewers and check that a notification was made
473 # Change reviewers and check that a notification was made
474 PullRequestModel().update_reviewers(
474 PullRequestModel().update_reviewers(
475 pull_request.pull_request_id, [(1, [], False, [])],
475 pull_request.pull_request_id, [(1, [], False, [])],
476 pull_request.author)
476 pull_request.author)
477 assert len(notifications.all()) == 2
477 assert len(notifications.all()) == 2
478
478
479 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
479 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
480 csrf_token):
480 csrf_token):
481 commits = [
481 commits = [
482 {'message': 'ancestor',
482 {'message': 'ancestor',
483 'added': [FileNode('file_A', content='content_of_ancestor')]},
483 'added': [FileNode('file_A', content='content_of_ancestor')]},
484 {'message': 'change',
484 {'message': 'change',
485 'added': [FileNode('file_a', content='content_of_change')]},
485 'added': [FileNode('file_a', content='content_of_change')]},
486 {'message': 'change-child'},
486 {'message': 'change-child'},
487 {'message': 'ancestor-child', 'parents': ['ancestor'],
487 {'message': 'ancestor-child', 'parents': ['ancestor'],
488 'added': [
488 'added': [
489 FileNode('file_B', content='content_of_ancestor_child')]},
489 FileNode('file_B', content='content_of_ancestor_child')]},
490 {'message': 'ancestor-child-2'},
490 {'message': 'ancestor-child-2'},
491 ]
491 ]
492 commit_ids = backend.create_master_repo(commits)
492 commit_ids = backend.create_master_repo(commits)
493 target = backend.create_repo(heads=['ancestor-child'])
493 target = backend.create_repo(heads=['ancestor-child'])
494 source = backend.create_repo(heads=['change'])
494 source = backend.create_repo(heads=['change'])
495
495
496 response = self.app.post(
496 response = self.app.post(
497 route_path('pullrequest_create', repo_name=source.repo_name),
497 route_path('pullrequest_create', repo_name=source.repo_name),
498 [
498 [
499 ('source_repo', source.repo_name),
499 ('source_repo', source.repo_name),
500 ('source_ref', 'branch:default:' + commit_ids['change']),
500 ('source_ref', 'branch:default:' + commit_ids['change']),
501 ('target_repo', target.repo_name),
501 ('target_repo', target.repo_name),
502 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
502 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
503 ('common_ancestor', commit_ids['ancestor']),
503 ('common_ancestor', commit_ids['ancestor']),
504 ('pullrequest_title', 'Title'),
504 ('pullrequest_title', 'Title'),
505 ('pullrequest_desc', 'Description'),
505 ('pullrequest_desc', 'Description'),
506 ('description_renderer', 'markdown'),
506 ('description_renderer', 'markdown'),
507 ('__start__', 'review_members:sequence'),
507 ('__start__', 'review_members:sequence'),
508 ('__start__', 'reviewer:mapping'),
508 ('__start__', 'reviewer:mapping'),
509 ('user_id', '1'),
509 ('user_id', '1'),
510 ('__start__', 'reasons:sequence'),
510 ('__start__', 'reasons:sequence'),
511 ('reason', 'Some reason'),
511 ('reason', 'Some reason'),
512 ('__end__', 'reasons:sequence'),
512 ('__end__', 'reasons:sequence'),
513 ('__start__', 'rules:sequence'),
513 ('__start__', 'rules:sequence'),
514 ('__end__', 'rules:sequence'),
514 ('__end__', 'rules:sequence'),
515 ('mandatory', 'False'),
515 ('mandatory', 'False'),
516 ('__end__', 'reviewer:mapping'),
516 ('__end__', 'reviewer:mapping'),
517 ('__end__', 'review_members:sequence'),
517 ('__end__', 'review_members:sequence'),
518 ('__start__', 'revisions:sequence'),
518 ('__start__', 'revisions:sequence'),
519 ('revisions', commit_ids['change']),
519 ('revisions', commit_ids['change']),
520 ('__end__', 'revisions:sequence'),
520 ('__end__', 'revisions:sequence'),
521 ('user', ''),
521 ('user', ''),
522 ('csrf_token', csrf_token),
522 ('csrf_token', csrf_token),
523 ],
523 ],
524 status=302)
524 status=302)
525
525
526 location = response.headers['Location']
526 location = response.headers['Location']
527
527
528 pull_request_id = location.rsplit('/', 1)[1]
528 pull_request_id = location.rsplit('/', 1)[1]
529 assert pull_request_id != 'new'
529 assert pull_request_id != 'new'
530 pull_request = PullRequest.get(int(pull_request_id))
530 pull_request = PullRequest.get(int(pull_request_id))
531
531
532 # target_ref has to point to the ancestor's commit_id in order to
532 # target_ref has to point to the ancestor's commit_id in order to
533 # show the correct diff
533 # show the correct diff
534 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
534 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
535 assert pull_request.target_ref == expected_target_ref
535 assert pull_request.target_ref == expected_target_ref
536
536
537 # Check generated diff contents
537 # Check generated diff contents
538 response = response.follow()
538 response = response.follow()
539 assert 'content_of_ancestor' not in response.body
539 assert 'content_of_ancestor' not in response.body
540 assert 'content_of_ancestor-child' not in response.body
540 assert 'content_of_ancestor-child' not in response.body
541 assert 'content_of_change' in response.body
541 assert 'content_of_change' in response.body
542
542
543 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
543 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
544 # Clear any previous calls to rcextensions
544 # Clear any previous calls to rcextensions
545 rhodecode.EXTENSIONS.calls.clear()
545 rhodecode.EXTENSIONS.calls.clear()
546
546
547 pull_request = pr_util.create_pull_request(
547 pull_request = pr_util.create_pull_request(
548 approved=True, mergeable=True)
548 approved=True, mergeable=True)
549 pull_request_id = pull_request.pull_request_id
549 pull_request_id = pull_request.pull_request_id
550 repo_name = pull_request.target_repo.scm_instance().name,
550 repo_name = pull_request.target_repo.scm_instance().name,
551
551
552 url = route_path('pullrequest_merge',
552 url = route_path('pullrequest_merge',
553 repo_name=str(repo_name[0]),
553 repo_name=str(repo_name[0]),
554 pull_request_id=pull_request_id)
554 pull_request_id=pull_request_id)
555 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
555 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
556
556
557 pull_request = PullRequest.get(pull_request_id)
557 pull_request = PullRequest.get(pull_request_id)
558
558
559 assert response.status_int == 200
559 assert response.status_int == 200
560 assert pull_request.is_closed()
560 assert pull_request.is_closed()
561 assert_pull_request_status(
561 assert_pull_request_status(
562 pull_request, ChangesetStatus.STATUS_APPROVED)
562 pull_request, ChangesetStatus.STATUS_APPROVED)
563
563
564 # Check the relevant log entries were added
564 # Check the relevant log entries were added
565 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
565 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
566 actions = [log.action for log in user_logs]
566 actions = [log.action for log in user_logs]
567 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
567 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
568 expected_actions = [
568 expected_actions = [
569 u'repo.pull_request.close',
569 u'repo.pull_request.close',
570 u'repo.pull_request.merge',
570 u'repo.pull_request.merge',
571 u'repo.pull_request.comment.create'
571 u'repo.pull_request.comment.create'
572 ]
572 ]
573 assert actions == expected_actions
573 assert actions == expected_actions
574
574
575 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
575 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
576 actions = [log for log in user_logs]
576 actions = [log for log in user_logs]
577 assert actions[-1].action == 'user.push'
577 assert actions[-1].action == 'user.push'
578 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
578 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
579
579
580 # Check post_push rcextension was really executed
580 # Check post_push rcextension was really executed
581 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
581 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
582 assert len(push_calls) == 1
582 assert len(push_calls) == 1
583 unused_last_call_args, last_call_kwargs = push_calls[0]
583 unused_last_call_args, last_call_kwargs = push_calls[0]
584 assert last_call_kwargs['action'] == 'push'
584 assert last_call_kwargs['action'] == 'push'
585 assert last_call_kwargs['commit_ids'] == pr_commit_ids
585 assert last_call_kwargs['commit_ids'] == pr_commit_ids
586
586
587 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
587 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
588 pull_request = pr_util.create_pull_request(mergeable=False)
588 pull_request = pr_util.create_pull_request(mergeable=False)
589 pull_request_id = pull_request.pull_request_id
589 pull_request_id = pull_request.pull_request_id
590 pull_request = PullRequest.get(pull_request_id)
590 pull_request = PullRequest.get(pull_request_id)
591
591
592 response = self.app.post(
592 response = self.app.post(
593 route_path('pullrequest_merge',
593 route_path('pullrequest_merge',
594 repo_name=pull_request.target_repo.scm_instance().name,
594 repo_name=pull_request.target_repo.scm_instance().name,
595 pull_request_id=pull_request.pull_request_id),
595 pull_request_id=pull_request.pull_request_id),
596 params={'csrf_token': csrf_token}).follow()
596 params={'csrf_token': csrf_token}).follow()
597
597
598 assert response.status_int == 200
598 assert response.status_int == 200
599 response.mustcontain(
599 response.mustcontain(
600 'Merge is not currently possible because of below failed checks.')
600 'Merge is not currently possible because of below failed checks.')
601 response.mustcontain('Server-side pull request merging is disabled.')
601 response.mustcontain('Server-side pull request merging is disabled.')
602
602
603 @pytest.mark.skip_backends('svn')
603 @pytest.mark.skip_backends('svn')
604 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
604 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
605 pull_request = pr_util.create_pull_request(mergeable=True)
605 pull_request = pr_util.create_pull_request(mergeable=True)
606 pull_request_id = pull_request.pull_request_id
606 pull_request_id = pull_request.pull_request_id
607 repo_name = pull_request.target_repo.scm_instance().name
607 repo_name = pull_request.target_repo.scm_instance().name
608
608
609 response = self.app.post(
609 response = self.app.post(
610 route_path('pullrequest_merge',
610 route_path('pullrequest_merge',
611 repo_name=repo_name, pull_request_id=pull_request_id),
611 repo_name=repo_name, pull_request_id=pull_request_id),
612 params={'csrf_token': csrf_token}).follow()
612 params={'csrf_token': csrf_token}).follow()
613
613
614 assert response.status_int == 200
614 assert response.status_int == 200
615
615
616 response.mustcontain(
616 response.mustcontain(
617 'Merge is not currently possible because of below failed checks.')
617 'Merge is not currently possible because of below failed checks.')
618 response.mustcontain('Pull request reviewer approval is pending.')
618 response.mustcontain('Pull request reviewer approval is pending.')
619
619
620 def test_merge_pull_request_renders_failure_reason(
620 def test_merge_pull_request_renders_failure_reason(
621 self, user_regular, csrf_token, pr_util):
621 self, user_regular, csrf_token, pr_util):
622 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
622 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
623 pull_request_id = pull_request.pull_request_id
623 pull_request_id = pull_request.pull_request_id
624 repo_name = pull_request.target_repo.scm_instance().name
624 repo_name = pull_request.target_repo.scm_instance().name
625
625
626 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
626 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
627 MergeFailureReason.PUSH_FAILED,
627 MergeFailureReason.PUSH_FAILED,
628 metadata={'target': 'shadow repo',
628 metadata={'target': 'shadow repo',
629 'merge_commit': 'xxx'})
629 'merge_commit': 'xxx'})
630 model_patcher = mock.patch.multiple(
630 model_patcher = mock.patch.multiple(
631 PullRequestModel,
631 PullRequestModel,
632 merge_repo=mock.Mock(return_value=merge_resp),
632 merge_repo=mock.Mock(return_value=merge_resp),
633 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
633 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
634
634
635 with model_patcher:
635 with model_patcher:
636 response = self.app.post(
636 response = self.app.post(
637 route_path('pullrequest_merge',
637 route_path('pullrequest_merge',
638 repo_name=repo_name,
638 repo_name=repo_name,
639 pull_request_id=pull_request_id),
639 pull_request_id=pull_request_id),
640 params={'csrf_token': csrf_token}, status=302)
640 params={'csrf_token': csrf_token}, status=302)
641
641
642 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
642 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
643 metadata={'target': 'shadow repo',
643 metadata={'target': 'shadow repo',
644 'merge_commit': 'xxx'})
644 'merge_commit': 'xxx'})
645 assert_session_flash(response, merge_resp.merge_status_message)
645 assert_session_flash(response, merge_resp.merge_status_message)
646
646
647 def test_update_source_revision(self, backend, csrf_token):
647 def test_update_source_revision(self, backend, csrf_token):
648 commits = [
648 commits = [
649 {'message': 'ancestor'},
649 {'message': 'ancestor'},
650 {'message': 'change'},
650 {'message': 'change'},
651 {'message': 'change-2'},
651 {'message': 'change-2'},
652 ]
652 ]
653 commit_ids = backend.create_master_repo(commits)
653 commit_ids = backend.create_master_repo(commits)
654 target = backend.create_repo(heads=['ancestor'])
654 target = backend.create_repo(heads=['ancestor'])
655 source = backend.create_repo(heads=['change'])
655 source = backend.create_repo(heads=['change'])
656
656
657 # create pr from a in source to A in target
657 # create pr from a in source to A in target
658 pull_request = PullRequest()
658 pull_request = PullRequest()
659
659
660 pull_request.source_repo = source
660 pull_request.source_repo = source
661 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
661 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
662 branch=backend.default_branch_name, commit_id=commit_ids['change'])
662 branch=backend.default_branch_name, commit_id=commit_ids['change'])
663
663
664 pull_request.target_repo = target
664 pull_request.target_repo = target
665 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
665 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
666 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
666 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
667
667
668 pull_request.revisions = [commit_ids['change']]
668 pull_request.revisions = [commit_ids['change']]
669 pull_request.title = u"Test"
669 pull_request.title = u"Test"
670 pull_request.description = u"Description"
670 pull_request.description = u"Description"
671 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
671 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
672 pull_request.pull_request_state = PullRequest.STATE_CREATED
672 pull_request.pull_request_state = PullRequest.STATE_CREATED
673 Session().add(pull_request)
673 Session().add(pull_request)
674 Session().commit()
674 Session().commit()
675 pull_request_id = pull_request.pull_request_id
675 pull_request_id = pull_request.pull_request_id
676
676
677 # source has ancestor - change - change-2
677 # source has ancestor - change - change-2
678 backend.pull_heads(source, heads=['change-2'])
678 backend.pull_heads(source, heads=['change-2'])
679
679
680 # update PR
680 # update PR
681 self.app.post(
681 self.app.post(
682 route_path('pullrequest_update',
682 route_path('pullrequest_update',
683 repo_name=target.repo_name, pull_request_id=pull_request_id),
683 repo_name=target.repo_name, pull_request_id=pull_request_id),
684 params={'update_commits': 'true', 'csrf_token': csrf_token})
684 params={'update_commits': 'true', 'csrf_token': csrf_token})
685
685
686 response = self.app.get(
686 response = self.app.get(
687 route_path('pullrequest_show',
687 route_path('pullrequest_show',
688 repo_name=target.repo_name,
688 repo_name=target.repo_name,
689 pull_request_id=pull_request.pull_request_id))
689 pull_request_id=pull_request.pull_request_id))
690
690
691 assert response.status_int == 200
691 assert response.status_int == 200
692 assert 'Pull request updated to' in response.body
692 assert 'Pull request updated to' in response.body
693 assert 'with 1 added, 0 removed commits.' in response.body
693 assert 'with 1 added, 0 removed commits.' in response.body
694
694
695 # check that we have now both revisions
695 # check that we have now both revisions
696 pull_request = PullRequest.get(pull_request_id)
696 pull_request = PullRequest.get(pull_request_id)
697 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
697 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
698
698
699 def test_update_target_revision(self, backend, csrf_token):
699 def test_update_target_revision(self, backend, csrf_token):
700 commits = [
700 commits = [
701 {'message': 'ancestor'},
701 {'message': 'ancestor'},
702 {'message': 'change'},
702 {'message': 'change'},
703 {'message': 'ancestor-new', 'parents': ['ancestor']},
703 {'message': 'ancestor-new', 'parents': ['ancestor']},
704 {'message': 'change-rebased'},
704 {'message': 'change-rebased'},
705 ]
705 ]
706 commit_ids = backend.create_master_repo(commits)
706 commit_ids = backend.create_master_repo(commits)
707 target = backend.create_repo(heads=['ancestor'])
707 target = backend.create_repo(heads=['ancestor'])
708 source = backend.create_repo(heads=['change'])
708 source = backend.create_repo(heads=['change'])
709
709
710 # create pr from a in source to A in target
710 # create pr from a in source to A in target
711 pull_request = PullRequest()
711 pull_request = PullRequest()
712
712
713 pull_request.source_repo = source
713 pull_request.source_repo = source
714 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
714 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
715 branch=backend.default_branch_name, commit_id=commit_ids['change'])
715 branch=backend.default_branch_name, commit_id=commit_ids['change'])
716
716
717 pull_request.target_repo = target
717 pull_request.target_repo = target
718 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
718 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
719 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
719 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
720
720
721 pull_request.revisions = [commit_ids['change']]
721 pull_request.revisions = [commit_ids['change']]
722 pull_request.title = u"Test"
722 pull_request.title = u"Test"
723 pull_request.description = u"Description"
723 pull_request.description = u"Description"
724 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
724 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
725 pull_request.pull_request_state = PullRequest.STATE_CREATED
725 pull_request.pull_request_state = PullRequest.STATE_CREATED
726
726
727 Session().add(pull_request)
727 Session().add(pull_request)
728 Session().commit()
728 Session().commit()
729 pull_request_id = pull_request.pull_request_id
729 pull_request_id = pull_request.pull_request_id
730
730
731 # target has ancestor - ancestor-new
731 # target has ancestor - ancestor-new
732 # source has ancestor - ancestor-new - change-rebased
732 # source has ancestor - ancestor-new - change-rebased
733 backend.pull_heads(target, heads=['ancestor-new'])
733 backend.pull_heads(target, heads=['ancestor-new'])
734 backend.pull_heads(source, heads=['change-rebased'])
734 backend.pull_heads(source, heads=['change-rebased'])
735
735
736 # update PR
736 # update PR
737 url = route_path('pullrequest_update',
737 url = route_path('pullrequest_update',
738 repo_name=target.repo_name,
738 repo_name=target.repo_name,
739 pull_request_id=pull_request_id)
739 pull_request_id=pull_request_id)
740 self.app.post(url,
740 self.app.post(url,
741 params={'update_commits': 'true', 'csrf_token': csrf_token},
741 params={'update_commits': 'true', 'csrf_token': csrf_token},
742 status=200)
742 status=200)
743
743
744 # check that we have now both revisions
744 # check that we have now both revisions
745 pull_request = PullRequest.get(pull_request_id)
745 pull_request = PullRequest.get(pull_request_id)
746 assert pull_request.revisions == [commit_ids['change-rebased']]
746 assert pull_request.revisions == [commit_ids['change-rebased']]
747 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
747 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
748 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
748 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
749
749
750 response = self.app.get(
750 response = self.app.get(
751 route_path('pullrequest_show',
751 route_path('pullrequest_show',
752 repo_name=target.repo_name,
752 repo_name=target.repo_name,
753 pull_request_id=pull_request.pull_request_id))
753 pull_request_id=pull_request.pull_request_id))
754 assert response.status_int == 200
754 assert response.status_int == 200
755 assert 'Pull request updated to' in response.body
755 assert 'Pull request updated to' in response.body
756 assert 'with 1 added, 1 removed commits.' in response.body
756 assert 'with 1 added, 1 removed commits.' in response.body
757
757
758 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
758 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
759 backend = backend_git
759 backend = backend_git
760 commits = [
760 commits = [
761 {'message': 'master-commit-1'},
761 {'message': 'master-commit-1'},
762 {'message': 'master-commit-2-change-1'},
762 {'message': 'master-commit-2-change-1'},
763 {'message': 'master-commit-3-change-2'},
763 {'message': 'master-commit-3-change-2'},
764
764
765 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
765 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
766 {'message': 'feat-commit-2'},
766 {'message': 'feat-commit-2'},
767 ]
767 ]
768 commit_ids = backend.create_master_repo(commits)
768 commit_ids = backend.create_master_repo(commits)
769 target = backend.create_repo(heads=['master-commit-3-change-2'])
769 target = backend.create_repo(heads=['master-commit-3-change-2'])
770 source = backend.create_repo(heads=['feat-commit-2'])
770 source = backend.create_repo(heads=['feat-commit-2'])
771
771
772 # create pr from a in source to A in target
772 # create pr from a in source to A in target
773 pull_request = PullRequest()
773 pull_request = PullRequest()
774 pull_request.source_repo = source
774 pull_request.source_repo = source
775
775
776 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
776 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
777 branch=backend.default_branch_name,
777 branch=backend.default_branch_name,
778 commit_id=commit_ids['master-commit-3-change-2'])
778 commit_id=commit_ids['master-commit-3-change-2'])
779
779
780 pull_request.target_repo = target
780 pull_request.target_repo = target
781 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
781 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
782 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
782 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
783
783
784 pull_request.revisions = [
784 pull_request.revisions = [
785 commit_ids['feat-commit-1'],
785 commit_ids['feat-commit-1'],
786 commit_ids['feat-commit-2']
786 commit_ids['feat-commit-2']
787 ]
787 ]
788 pull_request.title = u"Test"
788 pull_request.title = u"Test"
789 pull_request.description = u"Description"
789 pull_request.description = u"Description"
790 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
790 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
791 pull_request.pull_request_state = PullRequest.STATE_CREATED
791 pull_request.pull_request_state = PullRequest.STATE_CREATED
792 Session().add(pull_request)
792 Session().add(pull_request)
793 Session().commit()
793 Session().commit()
794 pull_request_id = pull_request.pull_request_id
794 pull_request_id = pull_request.pull_request_id
795
795
796 # PR is created, now we simulate a force-push into target,
796 # PR is created, now we simulate a force-push into target,
797 # that drops a 2 last commits
797 # that drops a 2 last commits
798 vcsrepo = target.scm_instance()
798 vcsrepo = target.scm_instance()
799 vcsrepo.config.clear_section('hooks')
799 vcsrepo.config.clear_section('hooks')
800 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
800 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
801
801
802 # update PR
802 # update PR
803 url = route_path('pullrequest_update',
803 url = route_path('pullrequest_update',
804 repo_name=target.repo_name,
804 repo_name=target.repo_name,
805 pull_request_id=pull_request_id)
805 pull_request_id=pull_request_id)
806 self.app.post(url,
806 self.app.post(url,
807 params={'update_commits': 'true', 'csrf_token': csrf_token},
807 params={'update_commits': 'true', 'csrf_token': csrf_token},
808 status=200)
808 status=200)
809
809
810 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
810 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
811 assert response.status_int == 200
811 assert response.status_int == 200
812 response.mustcontain('Pull request updated to')
812 response.mustcontain('Pull request updated to')
813 response.mustcontain('with 0 added, 0 removed commits.')
813 response.mustcontain('with 0 added, 0 removed commits.')
814
814
815 def test_update_of_ancestor_reference(self, backend, csrf_token):
815 def test_update_of_ancestor_reference(self, backend, csrf_token):
816 commits = [
816 commits = [
817 {'message': 'ancestor'},
817 {'message': 'ancestor'},
818 {'message': 'change'},
818 {'message': 'change'},
819 {'message': 'change-2'},
819 {'message': 'change-2'},
820 {'message': 'ancestor-new', 'parents': ['ancestor']},
820 {'message': 'ancestor-new', 'parents': ['ancestor']},
821 {'message': 'change-rebased'},
821 {'message': 'change-rebased'},
822 ]
822 ]
823 commit_ids = backend.create_master_repo(commits)
823 commit_ids = backend.create_master_repo(commits)
824 target = backend.create_repo(heads=['ancestor'])
824 target = backend.create_repo(heads=['ancestor'])
825 source = backend.create_repo(heads=['change'])
825 source = backend.create_repo(heads=['change'])
826
826
827 # create pr from a in source to A in target
827 # create pr from a in source to A in target
828 pull_request = PullRequest()
828 pull_request = PullRequest()
829 pull_request.source_repo = source
829 pull_request.source_repo = source
830
830
831 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
831 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
832 branch=backend.default_branch_name, commit_id=commit_ids['change'])
832 branch=backend.default_branch_name, commit_id=commit_ids['change'])
833 pull_request.target_repo = target
833 pull_request.target_repo = target
834 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
834 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
835 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
835 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
836 pull_request.revisions = [commit_ids['change']]
836 pull_request.revisions = [commit_ids['change']]
837 pull_request.title = u"Test"
837 pull_request.title = u"Test"
838 pull_request.description = u"Description"
838 pull_request.description = u"Description"
839 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
839 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
840 pull_request.pull_request_state = PullRequest.STATE_CREATED
840 pull_request.pull_request_state = PullRequest.STATE_CREATED
841 Session().add(pull_request)
841 Session().add(pull_request)
842 Session().commit()
842 Session().commit()
843 pull_request_id = pull_request.pull_request_id
843 pull_request_id = pull_request.pull_request_id
844
844
845 # target has ancestor - ancestor-new
845 # target has ancestor - ancestor-new
846 # source has ancestor - ancestor-new - change-rebased
846 # source has ancestor - ancestor-new - change-rebased
847 backend.pull_heads(target, heads=['ancestor-new'])
847 backend.pull_heads(target, heads=['ancestor-new'])
848 backend.pull_heads(source, heads=['change-rebased'])
848 backend.pull_heads(source, heads=['change-rebased'])
849
849
850 # update PR
850 # update PR
851 self.app.post(
851 self.app.post(
852 route_path('pullrequest_update',
852 route_path('pullrequest_update',
853 repo_name=target.repo_name, pull_request_id=pull_request_id),
853 repo_name=target.repo_name, pull_request_id=pull_request_id),
854 params={'update_commits': 'true', 'csrf_token': csrf_token},
854 params={'update_commits': 'true', 'csrf_token': csrf_token},
855 status=200)
855 status=200)
856
856
857 # Expect the target reference to be updated correctly
857 # Expect the target reference to be updated correctly
858 pull_request = PullRequest.get(pull_request_id)
858 pull_request = PullRequest.get(pull_request_id)
859 assert pull_request.revisions == [commit_ids['change-rebased']]
859 assert pull_request.revisions == [commit_ids['change-rebased']]
860 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
860 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
861 branch=backend.default_branch_name,
861 branch=backend.default_branch_name,
862 commit_id=commit_ids['ancestor-new'])
862 commit_id=commit_ids['ancestor-new'])
863 assert pull_request.target_ref == expected_target_ref
863 assert pull_request.target_ref == expected_target_ref
864
864
865 def test_remove_pull_request_branch(self, backend_git, csrf_token):
865 def test_remove_pull_request_branch(self, backend_git, csrf_token):
866 branch_name = 'development'
866 branch_name = 'development'
867 commits = [
867 commits = [
868 {'message': 'initial-commit'},
868 {'message': 'initial-commit'},
869 {'message': 'old-feature'},
869 {'message': 'old-feature'},
870 {'message': 'new-feature', 'branch': branch_name},
870 {'message': 'new-feature', 'branch': branch_name},
871 ]
871 ]
872 repo = backend_git.create_repo(commits)
872 repo = backend_git.create_repo(commits)
873 repo_name = repo.repo_name
873 repo_name = repo.repo_name
874 commit_ids = backend_git.commit_ids
874 commit_ids = backend_git.commit_ids
875
875
876 pull_request = PullRequest()
876 pull_request = PullRequest()
877 pull_request.source_repo = repo
877 pull_request.source_repo = repo
878 pull_request.target_repo = repo
878 pull_request.target_repo = repo
879 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
879 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
880 branch=branch_name, commit_id=commit_ids['new-feature'])
880 branch=branch_name, commit_id=commit_ids['new-feature'])
881 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
881 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
882 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
882 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
883 pull_request.revisions = [commit_ids['new-feature']]
883 pull_request.revisions = [commit_ids['new-feature']]
884 pull_request.title = u"Test"
884 pull_request.title = u"Test"
885 pull_request.description = u"Description"
885 pull_request.description = u"Description"
886 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
886 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
887 pull_request.pull_request_state = PullRequest.STATE_CREATED
887 pull_request.pull_request_state = PullRequest.STATE_CREATED
888 Session().add(pull_request)
888 Session().add(pull_request)
889 Session().commit()
889 Session().commit()
890
890
891 pull_request_id = pull_request.pull_request_id
891 pull_request_id = pull_request.pull_request_id
892
892
893 vcs = repo.scm_instance()
893 vcs = repo.scm_instance()
894 vcs.remove_ref('refs/heads/{}'.format(branch_name))
894 vcs.remove_ref('refs/heads/{}'.format(branch_name))
895
895
896 response = self.app.get(route_path(
896 response = self.app.get(route_path(
897 'pullrequest_show',
897 'pullrequest_show',
898 repo_name=repo_name,
898 repo_name=repo_name,
899 pull_request_id=pull_request_id))
899 pull_request_id=pull_request_id))
900
900
901 assert response.status_int == 200
901 assert response.status_int == 200
902
902
903 response.assert_response().element_contains(
903 response.assert_response().element_contains(
904 '#changeset_compare_view_content .alert strong',
904 '#changeset_compare_view_content .alert strong',
905 'Missing commits')
905 'Missing commits')
906 response.assert_response().element_contains(
906 response.assert_response().element_contains(
907 '#changeset_compare_view_content .alert',
907 '#changeset_compare_view_content .alert',
908 'This pull request cannot be displayed, because one or more'
908 'This pull request cannot be displayed, because one or more'
909 ' commits no longer exist in the source repository.')
909 ' commits no longer exist in the source repository.')
910
910
911 def test_strip_commits_from_pull_request(
911 def test_strip_commits_from_pull_request(
912 self, backend, pr_util, csrf_token):
912 self, backend, pr_util, csrf_token):
913 commits = [
913 commits = [
914 {'message': 'initial-commit'},
914 {'message': 'initial-commit'},
915 {'message': 'old-feature'},
915 {'message': 'old-feature'},
916 {'message': 'new-feature', 'parents': ['initial-commit']},
916 {'message': 'new-feature', 'parents': ['initial-commit']},
917 ]
917 ]
918 pull_request = pr_util.create_pull_request(
918 pull_request = pr_util.create_pull_request(
919 commits, target_head='initial-commit', source_head='new-feature',
919 commits, target_head='initial-commit', source_head='new-feature',
920 revisions=['new-feature'])
920 revisions=['new-feature'])
921
921
922 vcs = pr_util.source_repository.scm_instance()
922 vcs = pr_util.source_repository.scm_instance()
923 if backend.alias == 'git':
923 if backend.alias == 'git':
924 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
924 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
925 else:
925 else:
926 vcs.strip(pr_util.commit_ids['new-feature'])
926 vcs.strip(pr_util.commit_ids['new-feature'])
927
927
928 response = self.app.get(route_path(
928 response = self.app.get(route_path(
929 'pullrequest_show',
929 'pullrequest_show',
930 repo_name=pr_util.target_repository.repo_name,
930 repo_name=pr_util.target_repository.repo_name,
931 pull_request_id=pull_request.pull_request_id))
931 pull_request_id=pull_request.pull_request_id))
932
932
933 assert response.status_int == 200
933 assert response.status_int == 200
934
934
935 response.assert_response().element_contains(
935 response.assert_response().element_contains(
936 '#changeset_compare_view_content .alert strong',
936 '#changeset_compare_view_content .alert strong',
937 'Missing commits')
937 'Missing commits')
938 response.assert_response().element_contains(
938 response.assert_response().element_contains(
939 '#changeset_compare_view_content .alert',
939 '#changeset_compare_view_content .alert',
940 'This pull request cannot be displayed, because one or more'
940 'This pull request cannot be displayed, because one or more'
941 ' commits no longer exist in the source repository.')
941 ' commits no longer exist in the source repository.')
942 response.assert_response().element_contains(
942 response.assert_response().element_contains(
943 '#update_commits',
943 '#update_commits',
944 'Update commits')
944 'Update commits')
945
945
946 def test_strip_commits_and_update(
946 def test_strip_commits_and_update(
947 self, backend, pr_util, csrf_token):
947 self, backend, pr_util, csrf_token):
948 commits = [
948 commits = [
949 {'message': 'initial-commit'},
949 {'message': 'initial-commit'},
950 {'message': 'old-feature'},
950 {'message': 'old-feature'},
951 {'message': 'new-feature', 'parents': ['old-feature']},
951 {'message': 'new-feature', 'parents': ['old-feature']},
952 ]
952 ]
953 pull_request = pr_util.create_pull_request(
953 pull_request = pr_util.create_pull_request(
954 commits, target_head='old-feature', source_head='new-feature',
954 commits, target_head='old-feature', source_head='new-feature',
955 revisions=['new-feature'], mergeable=True)
955 revisions=['new-feature'], mergeable=True)
956
956
957 vcs = pr_util.source_repository.scm_instance()
957 vcs = pr_util.source_repository.scm_instance()
958 if backend.alias == 'git':
958 if backend.alias == 'git':
959 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
959 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
960 else:
960 else:
961 vcs.strip(pr_util.commit_ids['new-feature'])
961 vcs.strip(pr_util.commit_ids['new-feature'])
962
962
963 url = route_path('pullrequest_update',
963 url = route_path('pullrequest_update',
964 repo_name=pull_request.target_repo.repo_name,
964 repo_name=pull_request.target_repo.repo_name,
965 pull_request_id=pull_request.pull_request_id)
965 pull_request_id=pull_request.pull_request_id)
966 response = self.app.post(url,
966 response = self.app.post(url,
967 params={'update_commits': 'true',
967 params={'update_commits': 'true',
968 'csrf_token': csrf_token})
968 'csrf_token': csrf_token})
969
969
970 assert response.status_int == 200
970 assert response.status_int == 200
971 assert response.body == 'true'
971 assert response.body == '{"response": true, "redirect_url": null}'
972
972
973 # Make sure that after update, it won't raise 500 errors
973 # Make sure that after update, it won't raise 500 errors
974 response = self.app.get(route_path(
974 response = self.app.get(route_path(
975 'pullrequest_show',
975 'pullrequest_show',
976 repo_name=pr_util.target_repository.repo_name,
976 repo_name=pr_util.target_repository.repo_name,
977 pull_request_id=pull_request.pull_request_id))
977 pull_request_id=pull_request.pull_request_id))
978
978
979 assert response.status_int == 200
979 assert response.status_int == 200
980 response.assert_response().element_contains(
980 response.assert_response().element_contains(
981 '#changeset_compare_view_content .alert strong',
981 '#changeset_compare_view_content .alert strong',
982 'Missing commits')
982 'Missing commits')
983
983
984 def test_branch_is_a_link(self, pr_util):
984 def test_branch_is_a_link(self, pr_util):
985 pull_request = pr_util.create_pull_request()
985 pull_request = pr_util.create_pull_request()
986 pull_request.source_ref = 'branch:origin:1234567890abcdef'
986 pull_request.source_ref = 'branch:origin:1234567890abcdef'
987 pull_request.target_ref = 'branch:target:abcdef1234567890'
987 pull_request.target_ref = 'branch:target:abcdef1234567890'
988 Session().add(pull_request)
988 Session().add(pull_request)
989 Session().commit()
989 Session().commit()
990
990
991 response = self.app.get(route_path(
991 response = self.app.get(route_path(
992 'pullrequest_show',
992 'pullrequest_show',
993 repo_name=pull_request.target_repo.scm_instance().name,
993 repo_name=pull_request.target_repo.scm_instance().name,
994 pull_request_id=pull_request.pull_request_id))
994 pull_request_id=pull_request.pull_request_id))
995 assert response.status_int == 200
995 assert response.status_int == 200
996
996
997 origin = response.assert_response().get_element('.pr-origininfo .tag')
997 origin = response.assert_response().get_element('.pr-origininfo .tag')
998 origin_children = origin.getchildren()
998 origin_children = origin.getchildren()
999 assert len(origin_children) == 1
999 assert len(origin_children) == 1
1000 target = response.assert_response().get_element('.pr-targetinfo .tag')
1000 target = response.assert_response().get_element('.pr-targetinfo .tag')
1001 target_children = target.getchildren()
1001 target_children = target.getchildren()
1002 assert len(target_children) == 1
1002 assert len(target_children) == 1
1003
1003
1004 expected_origin_link = route_path(
1004 expected_origin_link = route_path(
1005 'repo_commits',
1005 'repo_commits',
1006 repo_name=pull_request.source_repo.scm_instance().name,
1006 repo_name=pull_request.source_repo.scm_instance().name,
1007 params=dict(branch='origin'))
1007 params=dict(branch='origin'))
1008 expected_target_link = route_path(
1008 expected_target_link = route_path(
1009 'repo_commits',
1009 'repo_commits',
1010 repo_name=pull_request.target_repo.scm_instance().name,
1010 repo_name=pull_request.target_repo.scm_instance().name,
1011 params=dict(branch='target'))
1011 params=dict(branch='target'))
1012 assert origin_children[0].attrib['href'] == expected_origin_link
1012 assert origin_children[0].attrib['href'] == expected_origin_link
1013 assert origin_children[0].text == 'branch: origin'
1013 assert origin_children[0].text == 'branch: origin'
1014 assert target_children[0].attrib['href'] == expected_target_link
1014 assert target_children[0].attrib['href'] == expected_target_link
1015 assert target_children[0].text == 'branch: target'
1015 assert target_children[0].text == 'branch: target'
1016
1016
1017 def test_bookmark_is_not_a_link(self, pr_util):
1017 def test_bookmark_is_not_a_link(self, pr_util):
1018 pull_request = pr_util.create_pull_request()
1018 pull_request = pr_util.create_pull_request()
1019 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1019 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1020 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1020 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1021 Session().add(pull_request)
1021 Session().add(pull_request)
1022 Session().commit()
1022 Session().commit()
1023
1023
1024 response = self.app.get(route_path(
1024 response = self.app.get(route_path(
1025 'pullrequest_show',
1025 'pullrequest_show',
1026 repo_name=pull_request.target_repo.scm_instance().name,
1026 repo_name=pull_request.target_repo.scm_instance().name,
1027 pull_request_id=pull_request.pull_request_id))
1027 pull_request_id=pull_request.pull_request_id))
1028 assert response.status_int == 200
1028 assert response.status_int == 200
1029
1029
1030 origin = response.assert_response().get_element('.pr-origininfo .tag')
1030 origin = response.assert_response().get_element('.pr-origininfo .tag')
1031 assert origin.text.strip() == 'bookmark: origin'
1031 assert origin.text.strip() == 'bookmark: origin'
1032 assert origin.getchildren() == []
1032 assert origin.getchildren() == []
1033
1033
1034 target = response.assert_response().get_element('.pr-targetinfo .tag')
1034 target = response.assert_response().get_element('.pr-targetinfo .tag')
1035 assert target.text.strip() == 'bookmark: target'
1035 assert target.text.strip() == 'bookmark: target'
1036 assert target.getchildren() == []
1036 assert target.getchildren() == []
1037
1037
1038 def test_tag_is_not_a_link(self, pr_util):
1038 def test_tag_is_not_a_link(self, pr_util):
1039 pull_request = pr_util.create_pull_request()
1039 pull_request = pr_util.create_pull_request()
1040 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1040 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1041 pull_request.target_ref = 'tag:target:abcdef1234567890'
1041 pull_request.target_ref = 'tag:target:abcdef1234567890'
1042 Session().add(pull_request)
1042 Session().add(pull_request)
1043 Session().commit()
1043 Session().commit()
1044
1044
1045 response = self.app.get(route_path(
1045 response = self.app.get(route_path(
1046 'pullrequest_show',
1046 'pullrequest_show',
1047 repo_name=pull_request.target_repo.scm_instance().name,
1047 repo_name=pull_request.target_repo.scm_instance().name,
1048 pull_request_id=pull_request.pull_request_id))
1048 pull_request_id=pull_request.pull_request_id))
1049 assert response.status_int == 200
1049 assert response.status_int == 200
1050
1050
1051 origin = response.assert_response().get_element('.pr-origininfo .tag')
1051 origin = response.assert_response().get_element('.pr-origininfo .tag')
1052 assert origin.text.strip() == 'tag: origin'
1052 assert origin.text.strip() == 'tag: origin'
1053 assert origin.getchildren() == []
1053 assert origin.getchildren() == []
1054
1054
1055 target = response.assert_response().get_element('.pr-targetinfo .tag')
1055 target = response.assert_response().get_element('.pr-targetinfo .tag')
1056 assert target.text.strip() == 'tag: target'
1056 assert target.text.strip() == 'tag: target'
1057 assert target.getchildren() == []
1057 assert target.getchildren() == []
1058
1058
1059 @pytest.mark.parametrize('mergeable', [True, False])
1059 @pytest.mark.parametrize('mergeable', [True, False])
1060 def test_shadow_repository_link(
1060 def test_shadow_repository_link(
1061 self, mergeable, pr_util, http_host_only_stub):
1061 self, mergeable, pr_util, http_host_only_stub):
1062 """
1062 """
1063 Check that the pull request summary page displays a link to the shadow
1063 Check that the pull request summary page displays a link to the shadow
1064 repository if the pull request is mergeable. If it is not mergeable
1064 repository if the pull request is mergeable. If it is not mergeable
1065 the link should not be displayed.
1065 the link should not be displayed.
1066 """
1066 """
1067 pull_request = pr_util.create_pull_request(
1067 pull_request = pr_util.create_pull_request(
1068 mergeable=mergeable, enable_notifications=False)
1068 mergeable=mergeable, enable_notifications=False)
1069 target_repo = pull_request.target_repo.scm_instance()
1069 target_repo = pull_request.target_repo.scm_instance()
1070 pr_id = pull_request.pull_request_id
1070 pr_id = pull_request.pull_request_id
1071 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1071 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1072 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1072 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1073
1073
1074 response = self.app.get(route_path(
1074 response = self.app.get(route_path(
1075 'pullrequest_show',
1075 'pullrequest_show',
1076 repo_name=target_repo.name,
1076 repo_name=target_repo.name,
1077 pull_request_id=pr_id))
1077 pull_request_id=pr_id))
1078
1078
1079 if mergeable:
1079 if mergeable:
1080 response.assert_response().element_value_contains(
1080 response.assert_response().element_value_contains(
1081 'input.pr-mergeinfo', shadow_url)
1081 'input.pr-mergeinfo', shadow_url)
1082 response.assert_response().element_value_contains(
1082 response.assert_response().element_value_contains(
1083 'input.pr-mergeinfo ', 'pr-merge')
1083 'input.pr-mergeinfo ', 'pr-merge')
1084 else:
1084 else:
1085 response.assert_response().no_element_exists('.pr-mergeinfo')
1085 response.assert_response().no_element_exists('.pr-mergeinfo')
1086
1086
1087
1087
1088 @pytest.mark.usefixtures('app')
1088 @pytest.mark.usefixtures('app')
1089 @pytest.mark.backends("git", "hg")
1089 @pytest.mark.backends("git", "hg")
1090 class TestPullrequestsControllerDelete(object):
1090 class TestPullrequestsControllerDelete(object):
1091 def test_pull_request_delete_button_permissions_admin(
1091 def test_pull_request_delete_button_permissions_admin(
1092 self, autologin_user, user_admin, pr_util):
1092 self, autologin_user, user_admin, pr_util):
1093 pull_request = pr_util.create_pull_request(
1093 pull_request = pr_util.create_pull_request(
1094 author=user_admin.username, enable_notifications=False)
1094 author=user_admin.username, enable_notifications=False)
1095
1095
1096 response = self.app.get(route_path(
1096 response = self.app.get(route_path(
1097 'pullrequest_show',
1097 'pullrequest_show',
1098 repo_name=pull_request.target_repo.scm_instance().name,
1098 repo_name=pull_request.target_repo.scm_instance().name,
1099 pull_request_id=pull_request.pull_request_id))
1099 pull_request_id=pull_request.pull_request_id))
1100
1100
1101 response.mustcontain('id="delete_pullrequest"')
1101 response.mustcontain('id="delete_pullrequest"')
1102 response.mustcontain('Confirm to delete this pull request')
1102 response.mustcontain('Confirm to delete this pull request')
1103
1103
1104 def test_pull_request_delete_button_permissions_owner(
1104 def test_pull_request_delete_button_permissions_owner(
1105 self, autologin_regular_user, user_regular, pr_util):
1105 self, autologin_regular_user, user_regular, pr_util):
1106 pull_request = pr_util.create_pull_request(
1106 pull_request = pr_util.create_pull_request(
1107 author=user_regular.username, enable_notifications=False)
1107 author=user_regular.username, enable_notifications=False)
1108
1108
1109 response = self.app.get(route_path(
1109 response = self.app.get(route_path(
1110 'pullrequest_show',
1110 'pullrequest_show',
1111 repo_name=pull_request.target_repo.scm_instance().name,
1111 repo_name=pull_request.target_repo.scm_instance().name,
1112 pull_request_id=pull_request.pull_request_id))
1112 pull_request_id=pull_request.pull_request_id))
1113
1113
1114 response.mustcontain('id="delete_pullrequest"')
1114 response.mustcontain('id="delete_pullrequest"')
1115 response.mustcontain('Confirm to delete this pull request')
1115 response.mustcontain('Confirm to delete this pull request')
1116
1116
1117 def test_pull_request_delete_button_permissions_forbidden(
1117 def test_pull_request_delete_button_permissions_forbidden(
1118 self, autologin_regular_user, user_regular, user_admin, pr_util):
1118 self, autologin_regular_user, user_regular, user_admin, pr_util):
1119 pull_request = pr_util.create_pull_request(
1119 pull_request = pr_util.create_pull_request(
1120 author=user_admin.username, enable_notifications=False)
1120 author=user_admin.username, enable_notifications=False)
1121
1121
1122 response = self.app.get(route_path(
1122 response = self.app.get(route_path(
1123 'pullrequest_show',
1123 'pullrequest_show',
1124 repo_name=pull_request.target_repo.scm_instance().name,
1124 repo_name=pull_request.target_repo.scm_instance().name,
1125 pull_request_id=pull_request.pull_request_id))
1125 pull_request_id=pull_request.pull_request_id))
1126 response.mustcontain(no=['id="delete_pullrequest"'])
1126 response.mustcontain(no=['id="delete_pullrequest"'])
1127 response.mustcontain(no=['Confirm to delete this pull request'])
1127 response.mustcontain(no=['Confirm to delete this pull request'])
1128
1128
1129 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1129 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1130 self, autologin_regular_user, user_regular, user_admin, pr_util,
1130 self, autologin_regular_user, user_regular, user_admin, pr_util,
1131 user_util):
1131 user_util):
1132
1132
1133 pull_request = pr_util.create_pull_request(
1133 pull_request = pr_util.create_pull_request(
1134 author=user_admin.username, enable_notifications=False)
1134 author=user_admin.username, enable_notifications=False)
1135
1135
1136 user_util.grant_user_permission_to_repo(
1136 user_util.grant_user_permission_to_repo(
1137 pull_request.target_repo, user_regular,
1137 pull_request.target_repo, user_regular,
1138 'repository.write')
1138 'repository.write')
1139
1139
1140 response = self.app.get(route_path(
1140 response = self.app.get(route_path(
1141 'pullrequest_show',
1141 'pullrequest_show',
1142 repo_name=pull_request.target_repo.scm_instance().name,
1142 repo_name=pull_request.target_repo.scm_instance().name,
1143 pull_request_id=pull_request.pull_request_id))
1143 pull_request_id=pull_request.pull_request_id))
1144
1144
1145 response.mustcontain('id="open_edit_pullrequest"')
1145 response.mustcontain('id="open_edit_pullrequest"')
1146 response.mustcontain('id="delete_pullrequest"')
1146 response.mustcontain('id="delete_pullrequest"')
1147 response.mustcontain(no=['Confirm to delete this pull request'])
1147 response.mustcontain(no=['Confirm to delete this pull request'])
1148
1148
1149 def test_delete_comment_returns_404_if_comment_does_not_exist(
1149 def test_delete_comment_returns_404_if_comment_does_not_exist(
1150 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1150 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1151
1151
1152 pull_request = pr_util.create_pull_request(
1152 pull_request = pr_util.create_pull_request(
1153 author=user_admin.username, enable_notifications=False)
1153 author=user_admin.username, enable_notifications=False)
1154
1154
1155 self.app.post(
1155 self.app.post(
1156 route_path(
1156 route_path(
1157 'pullrequest_comment_delete',
1157 'pullrequest_comment_delete',
1158 repo_name=pull_request.target_repo.scm_instance().name,
1158 repo_name=pull_request.target_repo.scm_instance().name,
1159 pull_request_id=pull_request.pull_request_id,
1159 pull_request_id=pull_request.pull_request_id,
1160 comment_id=1024404),
1160 comment_id=1024404),
1161 extra_environ=xhr_header,
1161 extra_environ=xhr_header,
1162 params={'csrf_token': csrf_token},
1162 params={'csrf_token': csrf_token},
1163 status=404
1163 status=404
1164 )
1164 )
1165
1165
1166 def test_delete_comment(
1166 def test_delete_comment(
1167 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1167 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1168
1168
1169 pull_request = pr_util.create_pull_request(
1169 pull_request = pr_util.create_pull_request(
1170 author=user_admin.username, enable_notifications=False)
1170 author=user_admin.username, enable_notifications=False)
1171 comment = pr_util.create_comment()
1171 comment = pr_util.create_comment()
1172 comment_id = comment.comment_id
1172 comment_id = comment.comment_id
1173
1173
1174 response = self.app.post(
1174 response = self.app.post(
1175 route_path(
1175 route_path(
1176 'pullrequest_comment_delete',
1176 'pullrequest_comment_delete',
1177 repo_name=pull_request.target_repo.scm_instance().name,
1177 repo_name=pull_request.target_repo.scm_instance().name,
1178 pull_request_id=pull_request.pull_request_id,
1178 pull_request_id=pull_request.pull_request_id,
1179 comment_id=comment_id),
1179 comment_id=comment_id),
1180 extra_environ=xhr_header,
1180 extra_environ=xhr_header,
1181 params={'csrf_token': csrf_token},
1181 params={'csrf_token': csrf_token},
1182 status=200
1182 status=200
1183 )
1183 )
1184 assert response.body == 'true'
1184 assert response.body == 'true'
1185
1185
1186 @pytest.mark.parametrize('url_type', [
1186 @pytest.mark.parametrize('url_type', [
1187 'pullrequest_new',
1187 'pullrequest_new',
1188 'pullrequest_create',
1188 'pullrequest_create',
1189 'pullrequest_update',
1189 'pullrequest_update',
1190 'pullrequest_merge',
1190 'pullrequest_merge',
1191 ])
1191 ])
1192 def test_pull_request_is_forbidden_on_archived_repo(
1192 def test_pull_request_is_forbidden_on_archived_repo(
1193 self, autologin_user, backend, xhr_header, user_util, url_type):
1193 self, autologin_user, backend, xhr_header, user_util, url_type):
1194
1194
1195 # create a temporary repo
1195 # create a temporary repo
1196 source = user_util.create_repo(repo_type=backend.alias)
1196 source = user_util.create_repo(repo_type=backend.alias)
1197 repo_name = source.repo_name
1197 repo_name = source.repo_name
1198 repo = Repository.get_by_repo_name(repo_name)
1198 repo = Repository.get_by_repo_name(repo_name)
1199 repo.archived = True
1199 repo.archived = True
1200 Session().commit()
1200 Session().commit()
1201
1201
1202 response = self.app.get(
1202 response = self.app.get(
1203 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1203 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1204
1204
1205 msg = 'Action not supported for archived repository.'
1205 msg = 'Action not supported for archived repository.'
1206 assert_session_flash(response, msg)
1206 assert_session_flash(response, msg)
1207
1207
1208
1208
1209 def assert_pull_request_status(pull_request, expected_status):
1209 def assert_pull_request_status(pull_request, expected_status):
1210 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1210 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1211 assert status == expected_status
1211 assert status == expected_status
1212
1212
1213
1213
1214 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1214 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1215 @pytest.mark.usefixtures("autologin_user")
1215 @pytest.mark.usefixtures("autologin_user")
1216 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1216 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1217 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1217 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,1470 +1,1481 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, EmptyRepositoryError)
44 RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 # backward compat., we use for OLD PRs a plain renderer
63 # backward compat., we use for OLD PRs a plain renderer
64 c.renderer = 'plain'
64 c.renderer = 'plain'
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, search_q=search_q, source=source, statuses=statuses,
83 repo_name, search_q=search_q, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, search_q=search_q, source=source, statuses=statuses,
100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.target_repo.repo_name),
111 pr.pull_request_id, pr.target_repo.repo_name),
112 'name_raw': pr.pull_request_id,
112 'name_raw': pr.pull_request_id,
113 'status': _render('pullrequest_status',
113 'status': _render('pullrequest_status',
114 pr.calculated_review_status()),
114 pr.calculated_review_status()),
115 'title': _render('pullrequest_title', pr.title, pr.description),
115 'title': _render('pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'state': pr.pull_request_state,
123 'state': pr.pull_request_state,
124 'author': _render('pullrequest_author',
124 'author': _render('pullrequest_author',
125 pr.author.full_contact, ),
125 pr.author.full_contact, ),
126 'author_raw': pr.author.full_name,
126 'author_raw': pr.author.full_name,
127 'comments': _render('pullrequest_comments', len(comments)),
127 'comments': _render('pullrequest_comments', len(comments)),
128 'comments_raw': len(comments),
128 'comments_raw': len(comments),
129 'closed': pr.is_closed(),
129 'closed': pr.is_closed(),
130 })
130 })
131
131
132 data = ({
132 data = ({
133 'draw': draw,
133 'draw': draw,
134 'data': data,
134 'data': data,
135 'recordsTotal': pull_requests_total_count,
135 'recordsTotal': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
137 })
137 })
138 return data
138 return data
139
139
140 @LoginRequired()
140 @LoginRequired()
141 @HasRepoPermissionAnyDecorator(
141 @HasRepoPermissionAnyDecorator(
142 'repository.read', 'repository.write', 'repository.admin')
142 'repository.read', 'repository.write', 'repository.admin')
143 @view_config(
143 @view_config(
144 route_name='pullrequest_show_all', request_method='GET',
144 route_name='pullrequest_show_all', request_method='GET',
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 def pull_request_list(self):
146 def pull_request_list(self):
147 c = self.load_default_context()
147 c = self.load_default_context()
148
148
149 req_get = self.request.GET
149 req_get = self.request.GET
150 c.source = str2bool(req_get.get('source'))
150 c.source = str2bool(req_get.get('source'))
151 c.closed = str2bool(req_get.get('closed'))
151 c.closed = str2bool(req_get.get('closed'))
152 c.my = str2bool(req_get.get('my'))
152 c.my = str2bool(req_get.get('my'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155
155
156 c.active = 'open'
156 c.active = 'open'
157 if c.my:
157 if c.my:
158 c.active = 'my'
158 c.active = 'my'
159 if c.closed:
159 if c.closed:
160 c.active = 'closed'
160 c.active = 'closed'
161 if c.awaiting_review and not c.source:
161 if c.awaiting_review and not c.source:
162 c.active = 'awaiting'
162 c.active = 'awaiting'
163 if c.source and not c.awaiting_review:
163 if c.source and not c.awaiting_review:
164 c.active = 'source'
164 c.active = 'source'
165 if c.awaiting_my_review:
165 if c.awaiting_my_review:
166 c.active = 'awaiting_my'
166 c.active = 'awaiting_my'
167
167
168 return self._get_template_context(c)
168 return self._get_template_context(c)
169
169
170 @LoginRequired()
170 @LoginRequired()
171 @HasRepoPermissionAnyDecorator(
171 @HasRepoPermissionAnyDecorator(
172 'repository.read', 'repository.write', 'repository.admin')
172 'repository.read', 'repository.write', 'repository.admin')
173 @view_config(
173 @view_config(
174 route_name='pullrequest_show_all_data', request_method='GET',
174 route_name='pullrequest_show_all_data', request_method='GET',
175 renderer='json_ext', xhr=True)
175 renderer='json_ext', xhr=True)
176 def pull_request_list_data(self):
176 def pull_request_list_data(self):
177 self.load_default_context()
177 self.load_default_context()
178
178
179 # additional filters
179 # additional filters
180 req_get = self.request.GET
180 req_get = self.request.GET
181 source = str2bool(req_get.get('source'))
181 source = str2bool(req_get.get('source'))
182 closed = str2bool(req_get.get('closed'))
182 closed = str2bool(req_get.get('closed'))
183 my = str2bool(req_get.get('my'))
183 my = str2bool(req_get.get('my'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186
186
187 filter_type = 'awaiting_review' if awaiting_review \
187 filter_type = 'awaiting_review' if awaiting_review \
188 else 'awaiting_my_review' if awaiting_my_review \
188 else 'awaiting_my_review' if awaiting_my_review \
189 else None
189 else None
190
190
191 opened_by = None
191 opened_by = None
192 if my:
192 if my:
193 opened_by = [self._rhodecode_user.user_id]
193 opened_by = [self._rhodecode_user.user_id]
194
194
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 if closed:
196 if closed:
197 statuses = [PullRequest.STATUS_CLOSED]
197 statuses = [PullRequest.STATUS_CLOSED]
198
198
199 data = self._get_pull_requests_list(
199 data = self._get_pull_requests_list(
200 repo_name=self.db_repo_name, source=source,
200 repo_name=self.db_repo_name, source=source,
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202
202
203 return data
203 return data
204
204
205 def _is_diff_cache_enabled(self, target_repo):
205 def _is_diff_cache_enabled(self, target_repo):
206 caching_enabled = self._get_general_setting(
206 caching_enabled = self._get_general_setting(
207 target_repo, 'rhodecode_diff_cache')
207 target_repo, 'rhodecode_diff_cache')
208 log.debug('Diff caching enabled: %s', caching_enabled)
208 log.debug('Diff caching enabled: %s', caching_enabled)
209 return caching_enabled
209 return caching_enabled
210
210
211 def _get_diffset(self, source_repo_name, source_repo,
211 def _get_diffset(self, source_repo_name, source_repo,
212 source_ref_id, target_ref_id,
212 source_ref_id, target_ref_id,
213 target_commit, source_commit, diff_limit, file_limit,
213 target_commit, source_commit, diff_limit, file_limit,
214 fulldiff, hide_whitespace_changes, diff_context):
214 fulldiff, hide_whitespace_changes, diff_context):
215
215
216 vcs_diff = PullRequestModel().get_diff(
216 vcs_diff = PullRequestModel().get_diff(
217 source_repo, source_ref_id, target_ref_id,
217 source_repo, source_ref_id, target_ref_id,
218 hide_whitespace_changes, diff_context)
218 hide_whitespace_changes, diff_context)
219
219
220 diff_processor = diffs.DiffProcessor(
220 diff_processor = diffs.DiffProcessor(
221 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 file_limit=file_limit, show_full_diff=fulldiff)
222 file_limit=file_limit, show_full_diff=fulldiff)
223
223
224 _parsed = diff_processor.prepare()
224 _parsed = diff_processor.prepare()
225
225
226 diffset = codeblocks.DiffSet(
226 diffset = codeblocks.DiffSet(
227 repo_name=self.db_repo_name,
227 repo_name=self.db_repo_name,
228 source_repo_name=source_repo_name,
228 source_repo_name=source_repo_name,
229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 )
231 )
232 diffset = self.path_filter.render_patchset_filtered(
232 diffset = self.path_filter.render_patchset_filtered(
233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234
234
235 return diffset
235 return diffset
236
236
237 def _get_range_diffset(self, source_scm, source_repo,
237 def _get_range_diffset(self, source_scm, source_repo,
238 commit1, commit2, diff_limit, file_limit,
238 commit1, commit2, diff_limit, file_limit,
239 fulldiff, hide_whitespace_changes, diff_context):
239 fulldiff, hide_whitespace_changes, diff_context):
240 vcs_diff = source_scm.get_diff(
240 vcs_diff = source_scm.get_diff(
241 commit1, commit2,
241 commit1, commit2,
242 ignore_whitespace=hide_whitespace_changes,
242 ignore_whitespace=hide_whitespace_changes,
243 context=diff_context)
243 context=diff_context)
244
244
245 diff_processor = diffs.DiffProcessor(
245 diff_processor = diffs.DiffProcessor(
246 vcs_diff, format='newdiff', diff_limit=diff_limit,
246 vcs_diff, format='newdiff', diff_limit=diff_limit,
247 file_limit=file_limit, show_full_diff=fulldiff)
247 file_limit=file_limit, show_full_diff=fulldiff)
248
248
249 _parsed = diff_processor.prepare()
249 _parsed = diff_processor.prepare()
250
250
251 diffset = codeblocks.DiffSet(
251 diffset = codeblocks.DiffSet(
252 repo_name=source_repo.repo_name,
252 repo_name=source_repo.repo_name,
253 source_node_getter=codeblocks.diffset_node_getter(commit1),
253 source_node_getter=codeblocks.diffset_node_getter(commit1),
254 target_node_getter=codeblocks.diffset_node_getter(commit2))
254 target_node_getter=codeblocks.diffset_node_getter(commit2))
255
255
256 diffset = self.path_filter.render_patchset_filtered(
256 diffset = self.path_filter.render_patchset_filtered(
257 diffset, _parsed, commit1.raw_id, commit2.raw_id)
257 diffset, _parsed, commit1.raw_id, commit2.raw_id)
258
258
259 return diffset
259 return diffset
260
260
261 @LoginRequired()
261 @LoginRequired()
262 @HasRepoPermissionAnyDecorator(
262 @HasRepoPermissionAnyDecorator(
263 'repository.read', 'repository.write', 'repository.admin')
263 'repository.read', 'repository.write', 'repository.admin')
264 @view_config(
264 @view_config(
265 route_name='pullrequest_show', request_method='GET',
265 route_name='pullrequest_show', request_method='GET',
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 def pull_request_show(self):
267 def pull_request_show(self):
268 _ = self.request.translate
268 _ = self.request.translate
269 c = self.load_default_context()
269 c = self.load_default_context()
270
270
271 pull_request = PullRequest.get_or_404(
271 pull_request = PullRequest.get_or_404(
272 self.request.matchdict['pull_request_id'])
272 self.request.matchdict['pull_request_id'])
273 pull_request_id = pull_request.pull_request_id
273 pull_request_id = pull_request.pull_request_id
274
274
275 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
275 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
276 log.debug('show: forbidden because pull request is in state %s',
276 log.debug('show: forbidden because pull request is in state %s',
277 pull_request.pull_request_state)
277 pull_request.pull_request_state)
278 msg = _(u'Cannot show pull requests in state other than `{}`. '
278 msg = _(u'Cannot show pull requests in state other than `{}`. '
279 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
279 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
280 pull_request.pull_request_state)
280 pull_request.pull_request_state)
281 h.flash(msg, category='error')
281 h.flash(msg, category='error')
282 raise HTTPFound(h.route_path('pullrequest_show_all',
282 raise HTTPFound(h.route_path('pullrequest_show_all',
283 repo_name=self.db_repo_name))
283 repo_name=self.db_repo_name))
284
284
285 version = self.request.GET.get('version')
285 version = self.request.GET.get('version')
286 from_version = self.request.GET.get('from_version') or version
286 from_version = self.request.GET.get('from_version') or version
287 merge_checks = self.request.GET.get('merge_checks')
287 merge_checks = self.request.GET.get('merge_checks')
288 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
288 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
289
289
290 # fetch global flags of ignore ws or context lines
290 # fetch global flags of ignore ws or context lines
291 diff_context = diffs.get_diff_context(self.request)
291 diff_context = diffs.get_diff_context(self.request)
292 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
292 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
293
293
294 force_refresh = str2bool(self.request.GET.get('force_refresh'))
294 force_refresh = str2bool(self.request.GET.get('force_refresh'))
295
295
296 (pull_request_latest,
296 (pull_request_latest,
297 pull_request_at_ver,
297 pull_request_at_ver,
298 pull_request_display_obj,
298 pull_request_display_obj,
299 at_version) = PullRequestModel().get_pr_version(
299 at_version) = PullRequestModel().get_pr_version(
300 pull_request_id, version=version)
300 pull_request_id, version=version)
301 pr_closed = pull_request_latest.is_closed()
301 pr_closed = pull_request_latest.is_closed()
302
302
303 if pr_closed and (version or from_version):
303 if pr_closed and (version or from_version):
304 # not allow to browse versions
304 # not allow to browse versions
305 raise HTTPFound(h.route_path(
305 raise HTTPFound(h.route_path(
306 'pullrequest_show', repo_name=self.db_repo_name,
306 'pullrequest_show', repo_name=self.db_repo_name,
307 pull_request_id=pull_request_id))
307 pull_request_id=pull_request_id))
308
308
309 versions = pull_request_display_obj.versions()
309 versions = pull_request_display_obj.versions()
310 # used to store per-commit range diffs
310 # used to store per-commit range diffs
311 c.changes = collections.OrderedDict()
311 c.changes = collections.OrderedDict()
312 c.range_diff_on = self.request.GET.get('range-diff') == "1"
312 c.range_diff_on = self.request.GET.get('range-diff') == "1"
313
313
314 c.at_version = at_version
314 c.at_version = at_version
315 c.at_version_num = (at_version
315 c.at_version_num = (at_version
316 if at_version and at_version != 'latest'
316 if at_version and at_version != 'latest'
317 else None)
317 else None)
318 c.at_version_pos = ChangesetComment.get_index_from_version(
318 c.at_version_pos = ChangesetComment.get_index_from_version(
319 c.at_version_num, versions)
319 c.at_version_num, versions)
320
320
321 (prev_pull_request_latest,
321 (prev_pull_request_latest,
322 prev_pull_request_at_ver,
322 prev_pull_request_at_ver,
323 prev_pull_request_display_obj,
323 prev_pull_request_display_obj,
324 prev_at_version) = PullRequestModel().get_pr_version(
324 prev_at_version) = PullRequestModel().get_pr_version(
325 pull_request_id, version=from_version)
325 pull_request_id, version=from_version)
326
326
327 c.from_version = prev_at_version
327 c.from_version = prev_at_version
328 c.from_version_num = (prev_at_version
328 c.from_version_num = (prev_at_version
329 if prev_at_version and prev_at_version != 'latest'
329 if prev_at_version and prev_at_version != 'latest'
330 else None)
330 else None)
331 c.from_version_pos = ChangesetComment.get_index_from_version(
331 c.from_version_pos = ChangesetComment.get_index_from_version(
332 c.from_version_num, versions)
332 c.from_version_num, versions)
333
333
334 # define if we're in COMPARE mode or VIEW at version mode
334 # define if we're in COMPARE mode or VIEW at version mode
335 compare = at_version != prev_at_version
335 compare = at_version != prev_at_version
336
336
337 # pull_requests repo_name we opened it against
337 # pull_requests repo_name we opened it against
338 # ie. target_repo must match
338 # ie. target_repo must match
339 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
339 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
340 raise HTTPNotFound()
340 raise HTTPNotFound()
341
341
342 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
342 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
343 pull_request_at_ver)
343 pull_request_at_ver)
344
344
345 c.pull_request = pull_request_display_obj
345 c.pull_request = pull_request_display_obj
346 c.renderer = pull_request_at_ver.description_renderer or c.renderer
346 c.renderer = pull_request_at_ver.description_renderer or c.renderer
347 c.pull_request_latest = pull_request_latest
347 c.pull_request_latest = pull_request_latest
348
348
349 if compare or (at_version and not at_version == 'latest'):
349 if compare or (at_version and not at_version == 'latest'):
350 c.allowed_to_change_status = False
350 c.allowed_to_change_status = False
351 c.allowed_to_update = False
351 c.allowed_to_update = False
352 c.allowed_to_merge = False
352 c.allowed_to_merge = False
353 c.allowed_to_delete = False
353 c.allowed_to_delete = False
354 c.allowed_to_comment = False
354 c.allowed_to_comment = False
355 c.allowed_to_close = False
355 c.allowed_to_close = False
356 else:
356 else:
357 can_change_status = PullRequestModel().check_user_change_status(
357 can_change_status = PullRequestModel().check_user_change_status(
358 pull_request_at_ver, self._rhodecode_user)
358 pull_request_at_ver, self._rhodecode_user)
359 c.allowed_to_change_status = can_change_status and not pr_closed
359 c.allowed_to_change_status = can_change_status and not pr_closed
360
360
361 c.allowed_to_update = PullRequestModel().check_user_update(
361 c.allowed_to_update = PullRequestModel().check_user_update(
362 pull_request_latest, self._rhodecode_user) and not pr_closed
362 pull_request_latest, self._rhodecode_user) and not pr_closed
363 c.allowed_to_merge = PullRequestModel().check_user_merge(
363 c.allowed_to_merge = PullRequestModel().check_user_merge(
364 pull_request_latest, self._rhodecode_user) and not pr_closed
364 pull_request_latest, self._rhodecode_user) and not pr_closed
365 c.allowed_to_delete = PullRequestModel().check_user_delete(
365 c.allowed_to_delete = PullRequestModel().check_user_delete(
366 pull_request_latest, self._rhodecode_user) and not pr_closed
366 pull_request_latest, self._rhodecode_user) and not pr_closed
367 c.allowed_to_comment = not pr_closed
367 c.allowed_to_comment = not pr_closed
368 c.allowed_to_close = c.allowed_to_merge and not pr_closed
368 c.allowed_to_close = c.allowed_to_merge and not pr_closed
369
369
370 c.forbid_adding_reviewers = False
370 c.forbid_adding_reviewers = False
371 c.forbid_author_to_review = False
371 c.forbid_author_to_review = False
372 c.forbid_commit_author_to_review = False
372 c.forbid_commit_author_to_review = False
373
373
374 if pull_request_latest.reviewer_data and \
374 if pull_request_latest.reviewer_data and \
375 'rules' in pull_request_latest.reviewer_data:
375 'rules' in pull_request_latest.reviewer_data:
376 rules = pull_request_latest.reviewer_data['rules'] or {}
376 rules = pull_request_latest.reviewer_data['rules'] or {}
377 try:
377 try:
378 c.forbid_adding_reviewers = rules.get(
378 c.forbid_adding_reviewers = rules.get(
379 'forbid_adding_reviewers')
379 'forbid_adding_reviewers')
380 c.forbid_author_to_review = rules.get(
380 c.forbid_author_to_review = rules.get(
381 'forbid_author_to_review')
381 'forbid_author_to_review')
382 c.forbid_commit_author_to_review = rules.get(
382 c.forbid_commit_author_to_review = rules.get(
383 'forbid_commit_author_to_review')
383 'forbid_commit_author_to_review')
384 except Exception:
384 except Exception:
385 pass
385 pass
386
386
387 # check merge capabilities
387 # check merge capabilities
388 _merge_check = MergeCheck.validate(
388 _merge_check = MergeCheck.validate(
389 pull_request_latest, auth_user=self._rhodecode_user,
389 pull_request_latest, auth_user=self._rhodecode_user,
390 translator=self.request.translate,
390 translator=self.request.translate,
391 force_shadow_repo_refresh=force_refresh)
391 force_shadow_repo_refresh=force_refresh)
392 c.pr_merge_errors = _merge_check.error_details
392 c.pr_merge_errors = _merge_check.error_details
393 c.pr_merge_possible = not _merge_check.failed
393 c.pr_merge_possible = not _merge_check.failed
394 c.pr_merge_message = _merge_check.merge_msg
394 c.pr_merge_message = _merge_check.merge_msg
395
395
396 c.pr_merge_info = MergeCheck.get_merge_conditions(
396 c.pr_merge_info = MergeCheck.get_merge_conditions(
397 pull_request_latest, translator=self.request.translate)
397 pull_request_latest, translator=self.request.translate)
398
398
399 c.pull_request_review_status = _merge_check.review_status
399 c.pull_request_review_status = _merge_check.review_status
400 if merge_checks:
400 if merge_checks:
401 self.request.override_renderer = \
401 self.request.override_renderer = \
402 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
402 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
403 return self._get_template_context(c)
403 return self._get_template_context(c)
404
404
405 comments_model = CommentsModel()
405 comments_model = CommentsModel()
406
406
407 # reviewers and statuses
407 # reviewers and statuses
408 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
408 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
409 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
409 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
410
410
411 # GENERAL COMMENTS with versions #
411 # GENERAL COMMENTS with versions #
412 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
412 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
413 q = q.order_by(ChangesetComment.comment_id.asc())
413 q = q.order_by(ChangesetComment.comment_id.asc())
414 general_comments = q
414 general_comments = q
415
415
416 # pick comments we want to render at current version
416 # pick comments we want to render at current version
417 c.comment_versions = comments_model.aggregate_comments(
417 c.comment_versions = comments_model.aggregate_comments(
418 general_comments, versions, c.at_version_num)
418 general_comments, versions, c.at_version_num)
419 c.comments = c.comment_versions[c.at_version_num]['until']
419 c.comments = c.comment_versions[c.at_version_num]['until']
420
420
421 # INLINE COMMENTS with versions #
421 # INLINE COMMENTS with versions #
422 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
422 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
423 q = q.order_by(ChangesetComment.comment_id.asc())
423 q = q.order_by(ChangesetComment.comment_id.asc())
424 inline_comments = q
424 inline_comments = q
425
425
426 c.inline_versions = comments_model.aggregate_comments(
426 c.inline_versions = comments_model.aggregate_comments(
427 inline_comments, versions, c.at_version_num, inline=True)
427 inline_comments, versions, c.at_version_num, inline=True)
428
428
429 # TODOs
429 # TODOs
430 c.unresolved_comments = CommentsModel() \
430 c.unresolved_comments = CommentsModel() \
431 .get_pull_request_unresolved_todos(pull_request)
431 .get_pull_request_unresolved_todos(pull_request)
432 c.resolved_comments = CommentsModel() \
432 c.resolved_comments = CommentsModel() \
433 .get_pull_request_resolved_todos(pull_request)
433 .get_pull_request_resolved_todos(pull_request)
434
434
435 # inject latest version
435 # inject latest version
436 latest_ver = PullRequest.get_pr_display_object(
436 latest_ver = PullRequest.get_pr_display_object(
437 pull_request_latest, pull_request_latest)
437 pull_request_latest, pull_request_latest)
438
438
439 c.versions = versions + [latest_ver]
439 c.versions = versions + [latest_ver]
440
440
441 # if we use version, then do not show later comments
441 # if we use version, then do not show later comments
442 # than current version
442 # than current version
443 display_inline_comments = collections.defaultdict(
443 display_inline_comments = collections.defaultdict(
444 lambda: collections.defaultdict(list))
444 lambda: collections.defaultdict(list))
445 for co in inline_comments:
445 for co in inline_comments:
446 if c.at_version_num:
446 if c.at_version_num:
447 # pick comments that are at least UPTO given version, so we
447 # pick comments that are at least UPTO given version, so we
448 # don't render comments for higher version
448 # don't render comments for higher version
449 should_render = co.pull_request_version_id and \
449 should_render = co.pull_request_version_id and \
450 co.pull_request_version_id <= c.at_version_num
450 co.pull_request_version_id <= c.at_version_num
451 else:
451 else:
452 # showing all, for 'latest'
452 # showing all, for 'latest'
453 should_render = True
453 should_render = True
454
454
455 if should_render:
455 if should_render:
456 display_inline_comments[co.f_path][co.line_no].append(co)
456 display_inline_comments[co.f_path][co.line_no].append(co)
457
457
458 # load diff data into template context, if we use compare mode then
458 # load diff data into template context, if we use compare mode then
459 # diff is calculated based on changes between versions of PR
459 # diff is calculated based on changes between versions of PR
460
460
461 source_repo = pull_request_at_ver.source_repo
461 source_repo = pull_request_at_ver.source_repo
462 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
462 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
463
463
464 target_repo = pull_request_at_ver.target_repo
464 target_repo = pull_request_at_ver.target_repo
465 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
465 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
466
466
467 if compare:
467 if compare:
468 # in compare switch the diff base to latest commit from prev version
468 # in compare switch the diff base to latest commit from prev version
469 target_ref_id = prev_pull_request_display_obj.revisions[0]
469 target_ref_id = prev_pull_request_display_obj.revisions[0]
470
470
471 # despite opening commits for bookmarks/branches/tags, we always
471 # despite opening commits for bookmarks/branches/tags, we always
472 # convert this to rev to prevent changes after bookmark or branch change
472 # convert this to rev to prevent changes after bookmark or branch change
473 c.source_ref_type = 'rev'
473 c.source_ref_type = 'rev'
474 c.source_ref = source_ref_id
474 c.source_ref = source_ref_id
475
475
476 c.target_ref_type = 'rev'
476 c.target_ref_type = 'rev'
477 c.target_ref = target_ref_id
477 c.target_ref = target_ref_id
478
478
479 c.source_repo = source_repo
479 c.source_repo = source_repo
480 c.target_repo = target_repo
480 c.target_repo = target_repo
481
481
482 c.commit_ranges = []
482 c.commit_ranges = []
483 source_commit = EmptyCommit()
483 source_commit = EmptyCommit()
484 target_commit = EmptyCommit()
484 target_commit = EmptyCommit()
485 c.missing_requirements = False
485 c.missing_requirements = False
486
486
487 source_scm = source_repo.scm_instance()
487 source_scm = source_repo.scm_instance()
488 target_scm = target_repo.scm_instance()
488 target_scm = target_repo.scm_instance()
489
489
490 shadow_scm = None
490 shadow_scm = None
491 try:
491 try:
492 shadow_scm = pull_request_latest.get_shadow_repo()
492 shadow_scm = pull_request_latest.get_shadow_repo()
493 except Exception:
493 except Exception:
494 log.debug('Failed to get shadow repo', exc_info=True)
494 log.debug('Failed to get shadow repo', exc_info=True)
495 # try first the existing source_repo, and then shadow
495 # try first the existing source_repo, and then shadow
496 # repo if we can obtain one
496 # repo if we can obtain one
497 commits_source_repo = source_scm or shadow_scm
497 commits_source_repo = source_scm or shadow_scm
498
498
499 c.commits_source_repo = commits_source_repo
499 c.commits_source_repo = commits_source_repo
500 c.ancestor = None # set it to None, to hide it from PR view
500 c.ancestor = None # set it to None, to hide it from PR view
501
501
502 # empty version means latest, so we keep this to prevent
502 # empty version means latest, so we keep this to prevent
503 # double caching
503 # double caching
504 version_normalized = version or 'latest'
504 version_normalized = version or 'latest'
505 from_version_normalized = from_version or 'latest'
505 from_version_normalized = from_version or 'latest'
506
506
507 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
507 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
508 cache_file_path = diff_cache_exist(
508 cache_file_path = diff_cache_exist(
509 cache_path, 'pull_request', pull_request_id, version_normalized,
509 cache_path, 'pull_request', pull_request_id, version_normalized,
510 from_version_normalized, source_ref_id, target_ref_id,
510 from_version_normalized, source_ref_id, target_ref_id,
511 hide_whitespace_changes, diff_context, c.fulldiff)
511 hide_whitespace_changes, diff_context, c.fulldiff)
512
512
513 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
513 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
514 force_recache = self.get_recache_flag()
514 force_recache = self.get_recache_flag()
515
515
516 cached_diff = None
516 cached_diff = None
517 if caching_enabled:
517 if caching_enabled:
518 cached_diff = load_cached_diff(cache_file_path)
518 cached_diff = load_cached_diff(cache_file_path)
519
519
520 has_proper_commit_cache = (
520 has_proper_commit_cache = (
521 cached_diff and cached_diff.get('commits')
521 cached_diff and cached_diff.get('commits')
522 and len(cached_diff.get('commits', [])) == 5
522 and len(cached_diff.get('commits', [])) == 5
523 and cached_diff.get('commits')[0]
523 and cached_diff.get('commits')[0]
524 and cached_diff.get('commits')[3])
524 and cached_diff.get('commits')[3])
525
525
526 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
526 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
527 diff_commit_cache = \
527 diff_commit_cache = \
528 (ancestor_commit, commit_cache, missing_requirements,
528 (ancestor_commit, commit_cache, missing_requirements,
529 source_commit, target_commit) = cached_diff['commits']
529 source_commit, target_commit) = cached_diff['commits']
530 else:
530 else:
531 diff_commit_cache = \
531 diff_commit_cache = \
532 (ancestor_commit, commit_cache, missing_requirements,
532 (ancestor_commit, commit_cache, missing_requirements,
533 source_commit, target_commit) = self.get_commits(
533 source_commit, target_commit) = self.get_commits(
534 commits_source_repo,
534 commits_source_repo,
535 pull_request_at_ver,
535 pull_request_at_ver,
536 source_commit,
536 source_commit,
537 source_ref_id,
537 source_ref_id,
538 source_scm,
538 source_scm,
539 target_commit,
539 target_commit,
540 target_ref_id,
540 target_ref_id,
541 target_scm)
541 target_scm)
542
542
543 # register our commit range
543 # register our commit range
544 for comm in commit_cache.values():
544 for comm in commit_cache.values():
545 c.commit_ranges.append(comm)
545 c.commit_ranges.append(comm)
546
546
547 c.missing_requirements = missing_requirements
547 c.missing_requirements = missing_requirements
548 c.ancestor_commit = ancestor_commit
548 c.ancestor_commit = ancestor_commit
549 c.statuses = source_repo.statuses(
549 c.statuses = source_repo.statuses(
550 [x.raw_id for x in c.commit_ranges])
550 [x.raw_id for x in c.commit_ranges])
551
551
552 # auto collapse if we have more than limit
552 # auto collapse if we have more than limit
553 collapse_limit = diffs.DiffProcessor._collapse_commits_over
553 collapse_limit = diffs.DiffProcessor._collapse_commits_over
554 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
554 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
555 c.compare_mode = compare
555 c.compare_mode = compare
556
556
557 # diff_limit is the old behavior, will cut off the whole diff
557 # diff_limit is the old behavior, will cut off the whole diff
558 # if the limit is applied otherwise will just hide the
558 # if the limit is applied otherwise will just hide the
559 # big files from the front-end
559 # big files from the front-end
560 diff_limit = c.visual.cut_off_limit_diff
560 diff_limit = c.visual.cut_off_limit_diff
561 file_limit = c.visual.cut_off_limit_file
561 file_limit = c.visual.cut_off_limit_file
562
562
563 c.missing_commits = False
563 c.missing_commits = False
564 if (c.missing_requirements
564 if (c.missing_requirements
565 or isinstance(source_commit, EmptyCommit)
565 or isinstance(source_commit, EmptyCommit)
566 or source_commit == target_commit):
566 or source_commit == target_commit):
567
567
568 c.missing_commits = True
568 c.missing_commits = True
569 else:
569 else:
570 c.inline_comments = display_inline_comments
570 c.inline_comments = display_inline_comments
571
571
572 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
572 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
573 if not force_recache and has_proper_diff_cache:
573 if not force_recache and has_proper_diff_cache:
574 c.diffset = cached_diff['diff']
574 c.diffset = cached_diff['diff']
575 (ancestor_commit, commit_cache, missing_requirements,
575 (ancestor_commit, commit_cache, missing_requirements,
576 source_commit, target_commit) = cached_diff['commits']
576 source_commit, target_commit) = cached_diff['commits']
577 else:
577 else:
578 c.diffset = self._get_diffset(
578 c.diffset = self._get_diffset(
579 c.source_repo.repo_name, commits_source_repo,
579 c.source_repo.repo_name, commits_source_repo,
580 source_ref_id, target_ref_id,
580 source_ref_id, target_ref_id,
581 target_commit, source_commit,
581 target_commit, source_commit,
582 diff_limit, file_limit, c.fulldiff,
582 diff_limit, file_limit, c.fulldiff,
583 hide_whitespace_changes, diff_context)
583 hide_whitespace_changes, diff_context)
584
584
585 # save cached diff
585 # save cached diff
586 if caching_enabled:
586 if caching_enabled:
587 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
587 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
588
588
589 c.limited_diff = c.diffset.limited_diff
589 c.limited_diff = c.diffset.limited_diff
590
590
591 # calculate removed files that are bound to comments
591 # calculate removed files that are bound to comments
592 comment_deleted_files = [
592 comment_deleted_files = [
593 fname for fname in display_inline_comments
593 fname for fname in display_inline_comments
594 if fname not in c.diffset.file_stats]
594 if fname not in c.diffset.file_stats]
595
595
596 c.deleted_files_comments = collections.defaultdict(dict)
596 c.deleted_files_comments = collections.defaultdict(dict)
597 for fname, per_line_comments in display_inline_comments.items():
597 for fname, per_line_comments in display_inline_comments.items():
598 if fname in comment_deleted_files:
598 if fname in comment_deleted_files:
599 c.deleted_files_comments[fname]['stats'] = 0
599 c.deleted_files_comments[fname]['stats'] = 0
600 c.deleted_files_comments[fname]['comments'] = list()
600 c.deleted_files_comments[fname]['comments'] = list()
601 for lno, comments in per_line_comments.items():
601 for lno, comments in per_line_comments.items():
602 c.deleted_files_comments[fname]['comments'].extend(comments)
602 c.deleted_files_comments[fname]['comments'].extend(comments)
603
603
604 # maybe calculate the range diff
604 # maybe calculate the range diff
605 if c.range_diff_on:
605 if c.range_diff_on:
606 # TODO(marcink): set whitespace/context
606 # TODO(marcink): set whitespace/context
607 context_lcl = 3
607 context_lcl = 3
608 ign_whitespace_lcl = False
608 ign_whitespace_lcl = False
609
609
610 for commit in c.commit_ranges:
610 for commit in c.commit_ranges:
611 commit2 = commit
611 commit2 = commit
612 commit1 = commit.first_parent
612 commit1 = commit.first_parent
613
613
614 range_diff_cache_file_path = diff_cache_exist(
614 range_diff_cache_file_path = diff_cache_exist(
615 cache_path, 'diff', commit.raw_id,
615 cache_path, 'diff', commit.raw_id,
616 ign_whitespace_lcl, context_lcl, c.fulldiff)
616 ign_whitespace_lcl, context_lcl, c.fulldiff)
617
617
618 cached_diff = None
618 cached_diff = None
619 if caching_enabled:
619 if caching_enabled:
620 cached_diff = load_cached_diff(range_diff_cache_file_path)
620 cached_diff = load_cached_diff(range_diff_cache_file_path)
621
621
622 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
622 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
623 if not force_recache and has_proper_diff_cache:
623 if not force_recache and has_proper_diff_cache:
624 diffset = cached_diff['diff']
624 diffset = cached_diff['diff']
625 else:
625 else:
626 diffset = self._get_range_diffset(
626 diffset = self._get_range_diffset(
627 source_scm, source_repo,
627 source_scm, source_repo,
628 commit1, commit2, diff_limit, file_limit,
628 commit1, commit2, diff_limit, file_limit,
629 c.fulldiff, ign_whitespace_lcl, context_lcl
629 c.fulldiff, ign_whitespace_lcl, context_lcl
630 )
630 )
631
631
632 # save cached diff
632 # save cached diff
633 if caching_enabled:
633 if caching_enabled:
634 cache_diff(range_diff_cache_file_path, diffset, None)
634 cache_diff(range_diff_cache_file_path, diffset, None)
635
635
636 c.changes[commit.raw_id] = diffset
636 c.changes[commit.raw_id] = diffset
637
637
638 # this is a hack to properly display links, when creating PR, the
638 # this is a hack to properly display links, when creating PR, the
639 # compare view and others uses different notation, and
639 # compare view and others uses different notation, and
640 # compare_commits.mako renders links based on the target_repo.
640 # compare_commits.mako renders links based on the target_repo.
641 # We need to swap that here to generate it properly on the html side
641 # We need to swap that here to generate it properly on the html side
642 c.target_repo = c.source_repo
642 c.target_repo = c.source_repo
643
643
644 c.commit_statuses = ChangesetStatus.STATUSES
644 c.commit_statuses = ChangesetStatus.STATUSES
645
645
646 c.show_version_changes = not pr_closed
646 c.show_version_changes = not pr_closed
647 if c.show_version_changes:
647 if c.show_version_changes:
648 cur_obj = pull_request_at_ver
648 cur_obj = pull_request_at_ver
649 prev_obj = prev_pull_request_at_ver
649 prev_obj = prev_pull_request_at_ver
650
650
651 old_commit_ids = prev_obj.revisions
651 old_commit_ids = prev_obj.revisions
652 new_commit_ids = cur_obj.revisions
652 new_commit_ids = cur_obj.revisions
653 commit_changes = PullRequestModel()._calculate_commit_id_changes(
653 commit_changes = PullRequestModel()._calculate_commit_id_changes(
654 old_commit_ids, new_commit_ids)
654 old_commit_ids, new_commit_ids)
655 c.commit_changes_summary = commit_changes
655 c.commit_changes_summary = commit_changes
656
656
657 # calculate the diff for commits between versions
657 # calculate the diff for commits between versions
658 c.commit_changes = []
658 c.commit_changes = []
659 mark = lambda cs, fw: list(
659 mark = lambda cs, fw: list(
660 h.itertools.izip_longest([], cs, fillvalue=fw))
660 h.itertools.izip_longest([], cs, fillvalue=fw))
661 for c_type, raw_id in mark(commit_changes.added, 'a') \
661 for c_type, raw_id in mark(commit_changes.added, 'a') \
662 + mark(commit_changes.removed, 'r') \
662 + mark(commit_changes.removed, 'r') \
663 + mark(commit_changes.common, 'c'):
663 + mark(commit_changes.common, 'c'):
664
664
665 if raw_id in commit_cache:
665 if raw_id in commit_cache:
666 commit = commit_cache[raw_id]
666 commit = commit_cache[raw_id]
667 else:
667 else:
668 try:
668 try:
669 commit = commits_source_repo.get_commit(raw_id)
669 commit = commits_source_repo.get_commit(raw_id)
670 except CommitDoesNotExistError:
670 except CommitDoesNotExistError:
671 # in case we fail extracting still use "dummy" commit
671 # in case we fail extracting still use "dummy" commit
672 # for display in commit diff
672 # for display in commit diff
673 commit = h.AttributeDict(
673 commit = h.AttributeDict(
674 {'raw_id': raw_id,
674 {'raw_id': raw_id,
675 'message': 'EMPTY or MISSING COMMIT'})
675 'message': 'EMPTY or MISSING COMMIT'})
676 c.commit_changes.append([c_type, commit])
676 c.commit_changes.append([c_type, commit])
677
677
678 # current user review statuses for each version
678 # current user review statuses for each version
679 c.review_versions = {}
679 c.review_versions = {}
680 if self._rhodecode_user.user_id in allowed_reviewers:
680 if self._rhodecode_user.user_id in allowed_reviewers:
681 for co in general_comments:
681 for co in general_comments:
682 if co.author.user_id == self._rhodecode_user.user_id:
682 if co.author.user_id == self._rhodecode_user.user_id:
683 status = co.status_change
683 status = co.status_change
684 if status:
684 if status:
685 _ver_pr = status[0].comment.pull_request_version_id
685 _ver_pr = status[0].comment.pull_request_version_id
686 c.review_versions[_ver_pr] = status[0]
686 c.review_versions[_ver_pr] = status[0]
687
687
688 return self._get_template_context(c)
688 return self._get_template_context(c)
689
689
690 def get_commits(
690 def get_commits(
691 self, commits_source_repo, pull_request_at_ver, source_commit,
691 self, commits_source_repo, pull_request_at_ver, source_commit,
692 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
692 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
693 commit_cache = collections.OrderedDict()
693 commit_cache = collections.OrderedDict()
694 missing_requirements = False
694 missing_requirements = False
695 try:
695 try:
696 pre_load = ["author", "date", "message", "branch", "parents"]
696 pre_load = ["author", "date", "message", "branch", "parents"]
697 show_revs = pull_request_at_ver.revisions
697 show_revs = pull_request_at_ver.revisions
698 for rev in show_revs:
698 for rev in show_revs:
699 comm = commits_source_repo.get_commit(
699 comm = commits_source_repo.get_commit(
700 commit_id=rev, pre_load=pre_load)
700 commit_id=rev, pre_load=pre_load)
701 commit_cache[comm.raw_id] = comm
701 commit_cache[comm.raw_id] = comm
702
702
703 # Order here matters, we first need to get target, and then
703 # Order here matters, we first need to get target, and then
704 # the source
704 # the source
705 target_commit = commits_source_repo.get_commit(
705 target_commit = commits_source_repo.get_commit(
706 commit_id=safe_str(target_ref_id))
706 commit_id=safe_str(target_ref_id))
707
707
708 source_commit = commits_source_repo.get_commit(
708 source_commit = commits_source_repo.get_commit(
709 commit_id=safe_str(source_ref_id))
709 commit_id=safe_str(source_ref_id))
710 except CommitDoesNotExistError:
710 except CommitDoesNotExistError:
711 log.warning(
711 log.warning(
712 'Failed to get commit from `{}` repo'.format(
712 'Failed to get commit from `{}` repo'.format(
713 commits_source_repo), exc_info=True)
713 commits_source_repo), exc_info=True)
714 except RepositoryRequirementError:
714 except RepositoryRequirementError:
715 log.warning(
715 log.warning(
716 'Failed to get all required data from repo', exc_info=True)
716 'Failed to get all required data from repo', exc_info=True)
717 missing_requirements = True
717 missing_requirements = True
718 ancestor_commit = None
718 ancestor_commit = None
719 try:
719 try:
720 ancestor_id = source_scm.get_common_ancestor(
720 ancestor_id = source_scm.get_common_ancestor(
721 source_commit.raw_id, target_commit.raw_id, target_scm)
721 source_commit.raw_id, target_commit.raw_id, target_scm)
722 ancestor_commit = source_scm.get_commit(ancestor_id)
722 ancestor_commit = source_scm.get_commit(ancestor_id)
723 except Exception:
723 except Exception:
724 ancestor_commit = None
724 ancestor_commit = None
725 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
725 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
726
726
727 def assure_not_empty_repo(self):
727 def assure_not_empty_repo(self):
728 _ = self.request.translate
728 _ = self.request.translate
729
729
730 try:
730 try:
731 self.db_repo.scm_instance().get_commit()
731 self.db_repo.scm_instance().get_commit()
732 except EmptyRepositoryError:
732 except EmptyRepositoryError:
733 h.flash(h.literal(_('There are no commits yet')),
733 h.flash(h.literal(_('There are no commits yet')),
734 category='warning')
734 category='warning')
735 raise HTTPFound(
735 raise HTTPFound(
736 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
736 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
737
737
738 @LoginRequired()
738 @LoginRequired()
739 @NotAnonymous()
739 @NotAnonymous()
740 @HasRepoPermissionAnyDecorator(
740 @HasRepoPermissionAnyDecorator(
741 'repository.read', 'repository.write', 'repository.admin')
741 'repository.read', 'repository.write', 'repository.admin')
742 @view_config(
742 @view_config(
743 route_name='pullrequest_new', request_method='GET',
743 route_name='pullrequest_new', request_method='GET',
744 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
744 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
745 def pull_request_new(self):
745 def pull_request_new(self):
746 _ = self.request.translate
746 _ = self.request.translate
747 c = self.load_default_context()
747 c = self.load_default_context()
748
748
749 self.assure_not_empty_repo()
749 self.assure_not_empty_repo()
750 source_repo = self.db_repo
750 source_repo = self.db_repo
751
751
752 commit_id = self.request.GET.get('commit')
752 commit_id = self.request.GET.get('commit')
753 branch_ref = self.request.GET.get('branch')
753 branch_ref = self.request.GET.get('branch')
754 bookmark_ref = self.request.GET.get('bookmark')
754 bookmark_ref = self.request.GET.get('bookmark')
755
755
756 try:
756 try:
757 source_repo_data = PullRequestModel().generate_repo_data(
757 source_repo_data = PullRequestModel().generate_repo_data(
758 source_repo, commit_id=commit_id,
758 source_repo, commit_id=commit_id,
759 branch=branch_ref, bookmark=bookmark_ref,
759 branch=branch_ref, bookmark=bookmark_ref,
760 translator=self.request.translate)
760 translator=self.request.translate)
761 except CommitDoesNotExistError as e:
761 except CommitDoesNotExistError as e:
762 log.exception(e)
762 log.exception(e)
763 h.flash(_('Commit does not exist'), 'error')
763 h.flash(_('Commit does not exist'), 'error')
764 raise HTTPFound(
764 raise HTTPFound(
765 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
765 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
766
766
767 default_target_repo = source_repo
767 default_target_repo = source_repo
768
768
769 if source_repo.parent and c.has_origin_repo_read_perm:
769 if source_repo.parent and c.has_origin_repo_read_perm:
770 parent_vcs_obj = source_repo.parent.scm_instance()
770 parent_vcs_obj = source_repo.parent.scm_instance()
771 if parent_vcs_obj and not parent_vcs_obj.is_empty():
771 if parent_vcs_obj and not parent_vcs_obj.is_empty():
772 # change default if we have a parent repo
772 # change default if we have a parent repo
773 default_target_repo = source_repo.parent
773 default_target_repo = source_repo.parent
774
774
775 target_repo_data = PullRequestModel().generate_repo_data(
775 target_repo_data = PullRequestModel().generate_repo_data(
776 default_target_repo, translator=self.request.translate)
776 default_target_repo, translator=self.request.translate)
777
777
778 selected_source_ref = source_repo_data['refs']['selected_ref']
778 selected_source_ref = source_repo_data['refs']['selected_ref']
779 title_source_ref = ''
779 title_source_ref = ''
780 if selected_source_ref:
780 if selected_source_ref:
781 title_source_ref = selected_source_ref.split(':', 2)[1]
781 title_source_ref = selected_source_ref.split(':', 2)[1]
782 c.default_title = PullRequestModel().generate_pullrequest_title(
782 c.default_title = PullRequestModel().generate_pullrequest_title(
783 source=source_repo.repo_name,
783 source=source_repo.repo_name,
784 source_ref=title_source_ref,
784 source_ref=title_source_ref,
785 target=default_target_repo.repo_name
785 target=default_target_repo.repo_name
786 )
786 )
787
787
788 c.default_repo_data = {
788 c.default_repo_data = {
789 'source_repo_name': source_repo.repo_name,
789 'source_repo_name': source_repo.repo_name,
790 'source_refs_json': json.dumps(source_repo_data),
790 'source_refs_json': json.dumps(source_repo_data),
791 'target_repo_name': default_target_repo.repo_name,
791 'target_repo_name': default_target_repo.repo_name,
792 'target_refs_json': json.dumps(target_repo_data),
792 'target_refs_json': json.dumps(target_repo_data),
793 }
793 }
794 c.default_source_ref = selected_source_ref
794 c.default_source_ref = selected_source_ref
795
795
796 return self._get_template_context(c)
796 return self._get_template_context(c)
797
797
798 @LoginRequired()
798 @LoginRequired()
799 @NotAnonymous()
799 @NotAnonymous()
800 @HasRepoPermissionAnyDecorator(
800 @HasRepoPermissionAnyDecorator(
801 'repository.read', 'repository.write', 'repository.admin')
801 'repository.read', 'repository.write', 'repository.admin')
802 @view_config(
802 @view_config(
803 route_name='pullrequest_repo_refs', request_method='GET',
803 route_name='pullrequest_repo_refs', request_method='GET',
804 renderer='json_ext', xhr=True)
804 renderer='json_ext', xhr=True)
805 def pull_request_repo_refs(self):
805 def pull_request_repo_refs(self):
806 self.load_default_context()
806 self.load_default_context()
807 target_repo_name = self.request.matchdict['target_repo_name']
807 target_repo_name = self.request.matchdict['target_repo_name']
808 repo = Repository.get_by_repo_name(target_repo_name)
808 repo = Repository.get_by_repo_name(target_repo_name)
809 if not repo:
809 if not repo:
810 raise HTTPNotFound()
810 raise HTTPNotFound()
811
811
812 target_perm = HasRepoPermissionAny(
812 target_perm = HasRepoPermissionAny(
813 'repository.read', 'repository.write', 'repository.admin')(
813 'repository.read', 'repository.write', 'repository.admin')(
814 target_repo_name)
814 target_repo_name)
815 if not target_perm:
815 if not target_perm:
816 raise HTTPNotFound()
816 raise HTTPNotFound()
817
817
818 return PullRequestModel().generate_repo_data(
818 return PullRequestModel().generate_repo_data(
819 repo, translator=self.request.translate)
819 repo, translator=self.request.translate)
820
820
821 @LoginRequired()
821 @LoginRequired()
822 @NotAnonymous()
822 @NotAnonymous()
823 @HasRepoPermissionAnyDecorator(
823 @HasRepoPermissionAnyDecorator(
824 'repository.read', 'repository.write', 'repository.admin')
824 'repository.read', 'repository.write', 'repository.admin')
825 @view_config(
825 @view_config(
826 route_name='pullrequest_repo_targets', request_method='GET',
826 route_name='pullrequest_repo_targets', request_method='GET',
827 renderer='json_ext', xhr=True)
827 renderer='json_ext', xhr=True)
828 def pullrequest_repo_targets(self):
828 def pullrequest_repo_targets(self):
829 _ = self.request.translate
829 _ = self.request.translate
830 filter_query = self.request.GET.get('query')
830 filter_query = self.request.GET.get('query')
831
831
832 # get the parents
832 # get the parents
833 parent_target_repos = []
833 parent_target_repos = []
834 if self.db_repo.parent:
834 if self.db_repo.parent:
835 parents_query = Repository.query() \
835 parents_query = Repository.query() \
836 .order_by(func.length(Repository.repo_name)) \
836 .order_by(func.length(Repository.repo_name)) \
837 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
837 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
838
838
839 if filter_query:
839 if filter_query:
840 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
840 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
841 parents_query = parents_query.filter(
841 parents_query = parents_query.filter(
842 Repository.repo_name.ilike(ilike_expression))
842 Repository.repo_name.ilike(ilike_expression))
843 parents = parents_query.limit(20).all()
843 parents = parents_query.limit(20).all()
844
844
845 for parent in parents:
845 for parent in parents:
846 parent_vcs_obj = parent.scm_instance()
846 parent_vcs_obj = parent.scm_instance()
847 if parent_vcs_obj and not parent_vcs_obj.is_empty():
847 if parent_vcs_obj and not parent_vcs_obj.is_empty():
848 parent_target_repos.append(parent)
848 parent_target_repos.append(parent)
849
849
850 # get other forks, and repo itself
850 # get other forks, and repo itself
851 query = Repository.query() \
851 query = Repository.query() \
852 .order_by(func.length(Repository.repo_name)) \
852 .order_by(func.length(Repository.repo_name)) \
853 .filter(
853 .filter(
854 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
854 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
855 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
855 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
856 ) \
856 ) \
857 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
857 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
858
858
859 if filter_query:
859 if filter_query:
860 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
860 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
861 query = query.filter(Repository.repo_name.ilike(ilike_expression))
861 query = query.filter(Repository.repo_name.ilike(ilike_expression))
862
862
863 limit = max(20 - len(parent_target_repos), 5) # not less then 5
863 limit = max(20 - len(parent_target_repos), 5) # not less then 5
864 target_repos = query.limit(limit).all()
864 target_repos = query.limit(limit).all()
865
865
866 all_target_repos = target_repos + parent_target_repos
866 all_target_repos = target_repos + parent_target_repos
867
867
868 repos = []
868 repos = []
869 # This checks permissions to the repositories
869 # This checks permissions to the repositories
870 for obj in ScmModel().get_repos(all_target_repos):
870 for obj in ScmModel().get_repos(all_target_repos):
871 repos.append({
871 repos.append({
872 'id': obj['name'],
872 'id': obj['name'],
873 'text': obj['name'],
873 'text': obj['name'],
874 'type': 'repo',
874 'type': 'repo',
875 'repo_id': obj['dbrepo']['repo_id'],
875 'repo_id': obj['dbrepo']['repo_id'],
876 'repo_type': obj['dbrepo']['repo_type'],
876 'repo_type': obj['dbrepo']['repo_type'],
877 'private': obj['dbrepo']['private'],
877 'private': obj['dbrepo']['private'],
878
878
879 })
879 })
880
880
881 data = {
881 data = {
882 'more': False,
882 'more': False,
883 'results': [{
883 'results': [{
884 'text': _('Repositories'),
884 'text': _('Repositories'),
885 'children': repos
885 'children': repos
886 }] if repos else []
886 }] if repos else []
887 }
887 }
888 return data
888 return data
889
889
890 @LoginRequired()
890 @LoginRequired()
891 @NotAnonymous()
891 @NotAnonymous()
892 @HasRepoPermissionAnyDecorator(
892 @HasRepoPermissionAnyDecorator(
893 'repository.read', 'repository.write', 'repository.admin')
893 'repository.read', 'repository.write', 'repository.admin')
894 @CSRFRequired()
894 @CSRFRequired()
895 @view_config(
895 @view_config(
896 route_name='pullrequest_create', request_method='POST',
896 route_name='pullrequest_create', request_method='POST',
897 renderer=None)
897 renderer=None)
898 def pull_request_create(self):
898 def pull_request_create(self):
899 _ = self.request.translate
899 _ = self.request.translate
900 self.assure_not_empty_repo()
900 self.assure_not_empty_repo()
901 self.load_default_context()
901 self.load_default_context()
902
902
903 controls = peppercorn.parse(self.request.POST.items())
903 controls = peppercorn.parse(self.request.POST.items())
904
904
905 try:
905 try:
906 form = PullRequestForm(
906 form = PullRequestForm(
907 self.request.translate, self.db_repo.repo_id)()
907 self.request.translate, self.db_repo.repo_id)()
908 _form = form.to_python(controls)
908 _form = form.to_python(controls)
909 except formencode.Invalid as errors:
909 except formencode.Invalid as errors:
910 if errors.error_dict.get('revisions'):
910 if errors.error_dict.get('revisions'):
911 msg = 'Revisions: %s' % errors.error_dict['revisions']
911 msg = 'Revisions: %s' % errors.error_dict['revisions']
912 elif errors.error_dict.get('pullrequest_title'):
912 elif errors.error_dict.get('pullrequest_title'):
913 msg = errors.error_dict.get('pullrequest_title')
913 msg = errors.error_dict.get('pullrequest_title')
914 else:
914 else:
915 msg = _('Error creating pull request: {}').format(errors)
915 msg = _('Error creating pull request: {}').format(errors)
916 log.exception(msg)
916 log.exception(msg)
917 h.flash(msg, 'error')
917 h.flash(msg, 'error')
918
918
919 # would rather just go back to form ...
919 # would rather just go back to form ...
920 raise HTTPFound(
920 raise HTTPFound(
921 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
921 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
922
922
923 source_repo = _form['source_repo']
923 source_repo = _form['source_repo']
924 source_ref = _form['source_ref']
924 source_ref = _form['source_ref']
925 target_repo = _form['target_repo']
925 target_repo = _form['target_repo']
926 target_ref = _form['target_ref']
926 target_ref = _form['target_ref']
927 commit_ids = _form['revisions'][::-1]
927 commit_ids = _form['revisions'][::-1]
928
928
929 # find the ancestor for this pr
929 # find the ancestor for this pr
930 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
930 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
931 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
931 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
932
932
933 if not (source_db_repo or target_db_repo):
933 if not (source_db_repo or target_db_repo):
934 h.flash(_('source_repo or target repo not found'), category='error')
934 h.flash(_('source_repo or target repo not found'), category='error')
935 raise HTTPFound(
935 raise HTTPFound(
936 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
936 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
937
937
938 # re-check permissions again here
938 # re-check permissions again here
939 # source_repo we must have read permissions
939 # source_repo we must have read permissions
940
940
941 source_perm = HasRepoPermissionAny(
941 source_perm = HasRepoPermissionAny(
942 'repository.read', 'repository.write', 'repository.admin')(
942 'repository.read', 'repository.write', 'repository.admin')(
943 source_db_repo.repo_name)
943 source_db_repo.repo_name)
944 if not source_perm:
944 if not source_perm:
945 msg = _('Not Enough permissions to source repo `{}`.'.format(
945 msg = _('Not Enough permissions to source repo `{}`.'.format(
946 source_db_repo.repo_name))
946 source_db_repo.repo_name))
947 h.flash(msg, category='error')
947 h.flash(msg, category='error')
948 # copy the args back to redirect
948 # copy the args back to redirect
949 org_query = self.request.GET.mixed()
949 org_query = self.request.GET.mixed()
950 raise HTTPFound(
950 raise HTTPFound(
951 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
951 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
952 _query=org_query))
952 _query=org_query))
953
953
954 # target repo we must have read permissions, and also later on
954 # target repo we must have read permissions, and also later on
955 # we want to check branch permissions here
955 # we want to check branch permissions here
956 target_perm = HasRepoPermissionAny(
956 target_perm = HasRepoPermissionAny(
957 'repository.read', 'repository.write', 'repository.admin')(
957 'repository.read', 'repository.write', 'repository.admin')(
958 target_db_repo.repo_name)
958 target_db_repo.repo_name)
959 if not target_perm:
959 if not target_perm:
960 msg = _('Not Enough permissions to target repo `{}`.'.format(
960 msg = _('Not Enough permissions to target repo `{}`.'.format(
961 target_db_repo.repo_name))
961 target_db_repo.repo_name))
962 h.flash(msg, category='error')
962 h.flash(msg, category='error')
963 # copy the args back to redirect
963 # copy the args back to redirect
964 org_query = self.request.GET.mixed()
964 org_query = self.request.GET.mixed()
965 raise HTTPFound(
965 raise HTTPFound(
966 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
966 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
967 _query=org_query))
967 _query=org_query))
968
968
969 source_scm = source_db_repo.scm_instance()
969 source_scm = source_db_repo.scm_instance()
970 target_scm = target_db_repo.scm_instance()
970 target_scm = target_db_repo.scm_instance()
971
971
972 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
972 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
973 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
973 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
974
974
975 ancestor = source_scm.get_common_ancestor(
975 ancestor = source_scm.get_common_ancestor(
976 source_commit.raw_id, target_commit.raw_id, target_scm)
976 source_commit.raw_id, target_commit.raw_id, target_scm)
977
977
978 # recalculate target ref based on ancestor
978 # recalculate target ref based on ancestor
979 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
979 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
980 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
980 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
981
981
982 get_default_reviewers_data, validate_default_reviewers = \
982 get_default_reviewers_data, validate_default_reviewers = \
983 PullRequestModel().get_reviewer_functions()
983 PullRequestModel().get_reviewer_functions()
984
984
985 # recalculate reviewers logic, to make sure we can validate this
985 # recalculate reviewers logic, to make sure we can validate this
986 reviewer_rules = get_default_reviewers_data(
986 reviewer_rules = get_default_reviewers_data(
987 self._rhodecode_db_user, source_db_repo,
987 self._rhodecode_db_user, source_db_repo,
988 source_commit, target_db_repo, target_commit)
988 source_commit, target_db_repo, target_commit)
989
989
990 given_reviewers = _form['review_members']
990 given_reviewers = _form['review_members']
991 reviewers = validate_default_reviewers(
991 reviewers = validate_default_reviewers(
992 given_reviewers, reviewer_rules)
992 given_reviewers, reviewer_rules)
993
993
994 pullrequest_title = _form['pullrequest_title']
994 pullrequest_title = _form['pullrequest_title']
995 title_source_ref = source_ref.split(':', 2)[1]
995 title_source_ref = source_ref.split(':', 2)[1]
996 if not pullrequest_title:
996 if not pullrequest_title:
997 pullrequest_title = PullRequestModel().generate_pullrequest_title(
997 pullrequest_title = PullRequestModel().generate_pullrequest_title(
998 source=source_repo,
998 source=source_repo,
999 source_ref=title_source_ref,
999 source_ref=title_source_ref,
1000 target=target_repo
1000 target=target_repo
1001 )
1001 )
1002
1002
1003 description = _form['pullrequest_desc']
1003 description = _form['pullrequest_desc']
1004 description_renderer = _form['description_renderer']
1004 description_renderer = _form['description_renderer']
1005
1005
1006 try:
1006 try:
1007 pull_request = PullRequestModel().create(
1007 pull_request = PullRequestModel().create(
1008 created_by=self._rhodecode_user.user_id,
1008 created_by=self._rhodecode_user.user_id,
1009 source_repo=source_repo,
1009 source_repo=source_repo,
1010 source_ref=source_ref,
1010 source_ref=source_ref,
1011 target_repo=target_repo,
1011 target_repo=target_repo,
1012 target_ref=target_ref,
1012 target_ref=target_ref,
1013 revisions=commit_ids,
1013 revisions=commit_ids,
1014 reviewers=reviewers,
1014 reviewers=reviewers,
1015 title=pullrequest_title,
1015 title=pullrequest_title,
1016 description=description,
1016 description=description,
1017 description_renderer=description_renderer,
1017 description_renderer=description_renderer,
1018 reviewer_data=reviewer_rules,
1018 reviewer_data=reviewer_rules,
1019 auth_user=self._rhodecode_user
1019 auth_user=self._rhodecode_user
1020 )
1020 )
1021 Session().commit()
1021 Session().commit()
1022
1022
1023 h.flash(_('Successfully opened new pull request'),
1023 h.flash(_('Successfully opened new pull request'),
1024 category='success')
1024 category='success')
1025 except Exception:
1025 except Exception:
1026 msg = _('Error occurred during creation of this pull request.')
1026 msg = _('Error occurred during creation of this pull request.')
1027 log.exception(msg)
1027 log.exception(msg)
1028 h.flash(msg, category='error')
1028 h.flash(msg, category='error')
1029
1029
1030 # copy the args back to redirect
1030 # copy the args back to redirect
1031 org_query = self.request.GET.mixed()
1031 org_query = self.request.GET.mixed()
1032 raise HTTPFound(
1032 raise HTTPFound(
1033 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1033 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1034 _query=org_query))
1034 _query=org_query))
1035
1035
1036 raise HTTPFound(
1036 raise HTTPFound(
1037 h.route_path('pullrequest_show', repo_name=target_repo,
1037 h.route_path('pullrequest_show', repo_name=target_repo,
1038 pull_request_id=pull_request.pull_request_id))
1038 pull_request_id=pull_request.pull_request_id))
1039
1039
1040 @LoginRequired()
1040 @LoginRequired()
1041 @NotAnonymous()
1041 @NotAnonymous()
1042 @HasRepoPermissionAnyDecorator(
1042 @HasRepoPermissionAnyDecorator(
1043 'repository.read', 'repository.write', 'repository.admin')
1043 'repository.read', 'repository.write', 'repository.admin')
1044 @CSRFRequired()
1044 @CSRFRequired()
1045 @view_config(
1045 @view_config(
1046 route_name='pullrequest_update', request_method='POST',
1046 route_name='pullrequest_update', request_method='POST',
1047 renderer='json_ext')
1047 renderer='json_ext')
1048 def pull_request_update(self):
1048 def pull_request_update(self):
1049 pull_request = PullRequest.get_or_404(
1049 pull_request = PullRequest.get_or_404(
1050 self.request.matchdict['pull_request_id'])
1050 self.request.matchdict['pull_request_id'])
1051 _ = self.request.translate
1051 _ = self.request.translate
1052
1052
1053 self.load_default_context()
1053 self.load_default_context()
1054 redirect_url = None
1054
1055
1055 if pull_request.is_closed():
1056 if pull_request.is_closed():
1056 log.debug('update: forbidden because pull request is closed')
1057 log.debug('update: forbidden because pull request is closed')
1057 msg = _(u'Cannot update closed pull requests.')
1058 msg = _(u'Cannot update closed pull requests.')
1058 h.flash(msg, category='error')
1059 h.flash(msg, category='error')
1059 return True
1060 return {'response': True,
1061 'redirect_url': redirect_url}
1060
1062
1061 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1063 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1062 log.debug('update: forbidden because pull request is in state %s',
1064 log.debug('update: forbidden because pull request is in state %s',
1063 pull_request.pull_request_state)
1065 pull_request.pull_request_state)
1064 msg = _(u'Cannot update pull requests in state other than `{}`. '
1066 msg = _(u'Cannot update pull requests in state other than `{}`. '
1065 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1067 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1066 pull_request.pull_request_state)
1068 pull_request.pull_request_state)
1067 h.flash(msg, category='error')
1069 h.flash(msg, category='error')
1068 return True
1070 return {'response': True,
1071 'redirect_url': redirect_url}
1069
1072
1070 # only owner or admin can update it
1073 # only owner or admin can update it
1071 allowed_to_update = PullRequestModel().check_user_update(
1074 allowed_to_update = PullRequestModel().check_user_update(
1072 pull_request, self._rhodecode_user)
1075 pull_request, self._rhodecode_user)
1073 if allowed_to_update:
1076 if allowed_to_update:
1074 controls = peppercorn.parse(self.request.POST.items())
1077 controls = peppercorn.parse(self.request.POST.items())
1078 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1075
1079
1076 if 'review_members' in controls:
1080 if 'review_members' in controls:
1077 self._update_reviewers(
1081 self._update_reviewers(
1078 pull_request, controls['review_members'],
1082 pull_request, controls['review_members'],
1079 pull_request.reviewer_data)
1083 pull_request.reviewer_data)
1080 elif str2bool(self.request.POST.get('update_commits', 'false')):
1084 elif str2bool(self.request.POST.get('update_commits', 'false')):
1081 self._update_commits(pull_request)
1085 self._update_commits(pull_request)
1086 if force_refresh:
1087 redirect_url = h.route_path(
1088 'pullrequest_show', repo_name=self.db_repo_name,
1089 pull_request_id=pull_request.pull_request_id,
1090 _query={"force_refresh": 1})
1082 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1091 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1083 self._edit_pull_request(pull_request)
1092 self._edit_pull_request(pull_request)
1084 else:
1093 else:
1085 raise HTTPBadRequest()
1094 raise HTTPBadRequest()
1086 return True
1095
1096 return {'response': True,
1097 'redirect_url': redirect_url}
1087 raise HTTPForbidden()
1098 raise HTTPForbidden()
1088
1099
1089 def _edit_pull_request(self, pull_request):
1100 def _edit_pull_request(self, pull_request):
1090 _ = self.request.translate
1101 _ = self.request.translate
1091
1102
1092 try:
1103 try:
1093 PullRequestModel().edit(
1104 PullRequestModel().edit(
1094 pull_request,
1105 pull_request,
1095 self.request.POST.get('title'),
1106 self.request.POST.get('title'),
1096 self.request.POST.get('description'),
1107 self.request.POST.get('description'),
1097 self.request.POST.get('description_renderer'),
1108 self.request.POST.get('description_renderer'),
1098 self._rhodecode_user)
1109 self._rhodecode_user)
1099 except ValueError:
1110 except ValueError:
1100 msg = _(u'Cannot update closed pull requests.')
1111 msg = _(u'Cannot update closed pull requests.')
1101 h.flash(msg, category='error')
1112 h.flash(msg, category='error')
1102 return
1113 return
1103 else:
1114 else:
1104 Session().commit()
1115 Session().commit()
1105
1116
1106 msg = _(u'Pull request title & description updated.')
1117 msg = _(u'Pull request title & description updated.')
1107 h.flash(msg, category='success')
1118 h.flash(msg, category='success')
1108 return
1119 return
1109
1120
1110 def _update_commits(self, pull_request):
1121 def _update_commits(self, pull_request):
1111 _ = self.request.translate
1122 _ = self.request.translate
1112
1123
1113 with pull_request.set_state(PullRequest.STATE_UPDATING):
1124 with pull_request.set_state(PullRequest.STATE_UPDATING):
1114 resp = PullRequestModel().update_commits(pull_request)
1125 resp = PullRequestModel().update_commits(pull_request)
1115
1126
1116 if resp.executed:
1127 if resp.executed:
1117
1128
1118 if resp.target_changed and resp.source_changed:
1129 if resp.target_changed and resp.source_changed:
1119 changed = 'target and source repositories'
1130 changed = 'target and source repositories'
1120 elif resp.target_changed and not resp.source_changed:
1131 elif resp.target_changed and not resp.source_changed:
1121 changed = 'target repository'
1132 changed = 'target repository'
1122 elif not resp.target_changed and resp.source_changed:
1133 elif not resp.target_changed and resp.source_changed:
1123 changed = 'source repository'
1134 changed = 'source repository'
1124 else:
1135 else:
1125 changed = 'nothing'
1136 changed = 'nothing'
1126
1137
1127 msg = _(u'Pull request updated to "{source_commit_id}" with '
1138 msg = _(u'Pull request updated to "{source_commit_id}" with '
1128 u'{count_added} added, {count_removed} removed commits. '
1139 u'{count_added} added, {count_removed} removed commits. '
1129 u'Source of changes: {change_source}')
1140 u'Source of changes: {change_source}')
1130 msg = msg.format(
1141 msg = msg.format(
1131 source_commit_id=pull_request.source_ref_parts.commit_id,
1142 source_commit_id=pull_request.source_ref_parts.commit_id,
1132 count_added=len(resp.changes.added),
1143 count_added=len(resp.changes.added),
1133 count_removed=len(resp.changes.removed),
1144 count_removed=len(resp.changes.removed),
1134 change_source=changed)
1145 change_source=changed)
1135 h.flash(msg, category='success')
1146 h.flash(msg, category='success')
1136
1147
1137 channel = '/repo${}$/pr/{}'.format(
1148 channel = '/repo${}$/pr/{}'.format(
1138 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1149 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1139 message = msg + (
1150 message = msg + (
1140 ' - <a onclick="window.location.reload()">'
1151 ' - <a onclick="window.location.reload()">'
1141 '<strong>{}</strong></a>'.format(_('Reload page')))
1152 '<strong>{}</strong></a>'.format(_('Reload page')))
1142 channelstream.post_message(
1153 channelstream.post_message(
1143 channel, message, self._rhodecode_user.username,
1154 channel, message, self._rhodecode_user.username,
1144 registry=self.request.registry)
1155 registry=self.request.registry)
1145 else:
1156 else:
1146 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1157 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1147 warning_reasons = [
1158 warning_reasons = [
1148 UpdateFailureReason.NO_CHANGE,
1159 UpdateFailureReason.NO_CHANGE,
1149 UpdateFailureReason.WRONG_REF_TYPE,
1160 UpdateFailureReason.WRONG_REF_TYPE,
1150 ]
1161 ]
1151 category = 'warning' if resp.reason in warning_reasons else 'error'
1162 category = 'warning' if resp.reason in warning_reasons else 'error'
1152 h.flash(msg, category=category)
1163 h.flash(msg, category=category)
1153
1164
1154 @LoginRequired()
1165 @LoginRequired()
1155 @NotAnonymous()
1166 @NotAnonymous()
1156 @HasRepoPermissionAnyDecorator(
1167 @HasRepoPermissionAnyDecorator(
1157 'repository.read', 'repository.write', 'repository.admin')
1168 'repository.read', 'repository.write', 'repository.admin')
1158 @CSRFRequired()
1169 @CSRFRequired()
1159 @view_config(
1170 @view_config(
1160 route_name='pullrequest_merge', request_method='POST',
1171 route_name='pullrequest_merge', request_method='POST',
1161 renderer='json_ext')
1172 renderer='json_ext')
1162 def pull_request_merge(self):
1173 def pull_request_merge(self):
1163 """
1174 """
1164 Merge will perform a server-side merge of the specified
1175 Merge will perform a server-side merge of the specified
1165 pull request, if the pull request is approved and mergeable.
1176 pull request, if the pull request is approved and mergeable.
1166 After successful merging, the pull request is automatically
1177 After successful merging, the pull request is automatically
1167 closed, with a relevant comment.
1178 closed, with a relevant comment.
1168 """
1179 """
1169 pull_request = PullRequest.get_or_404(
1180 pull_request = PullRequest.get_or_404(
1170 self.request.matchdict['pull_request_id'])
1181 self.request.matchdict['pull_request_id'])
1171 _ = self.request.translate
1182 _ = self.request.translate
1172
1183
1173 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1184 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1174 log.debug('show: forbidden because pull request is in state %s',
1185 log.debug('show: forbidden because pull request is in state %s',
1175 pull_request.pull_request_state)
1186 pull_request.pull_request_state)
1176 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1187 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1177 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1188 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1178 pull_request.pull_request_state)
1189 pull_request.pull_request_state)
1179 h.flash(msg, category='error')
1190 h.flash(msg, category='error')
1180 raise HTTPFound(
1191 raise HTTPFound(
1181 h.route_path('pullrequest_show',
1192 h.route_path('pullrequest_show',
1182 repo_name=pull_request.target_repo.repo_name,
1193 repo_name=pull_request.target_repo.repo_name,
1183 pull_request_id=pull_request.pull_request_id))
1194 pull_request_id=pull_request.pull_request_id))
1184
1195
1185 self.load_default_context()
1196 self.load_default_context()
1186
1197
1187 with pull_request.set_state(PullRequest.STATE_UPDATING):
1198 with pull_request.set_state(PullRequest.STATE_UPDATING):
1188 check = MergeCheck.validate(
1199 check = MergeCheck.validate(
1189 pull_request, auth_user=self._rhodecode_user,
1200 pull_request, auth_user=self._rhodecode_user,
1190 translator=self.request.translate)
1201 translator=self.request.translate)
1191 merge_possible = not check.failed
1202 merge_possible = not check.failed
1192
1203
1193 for err_type, error_msg in check.errors:
1204 for err_type, error_msg in check.errors:
1194 h.flash(error_msg, category=err_type)
1205 h.flash(error_msg, category=err_type)
1195
1206
1196 if merge_possible:
1207 if merge_possible:
1197 log.debug("Pre-conditions checked, trying to merge.")
1208 log.debug("Pre-conditions checked, trying to merge.")
1198 extras = vcs_operation_context(
1209 extras = vcs_operation_context(
1199 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1210 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1200 username=self._rhodecode_db_user.username, action='push',
1211 username=self._rhodecode_db_user.username, action='push',
1201 scm=pull_request.target_repo.repo_type)
1212 scm=pull_request.target_repo.repo_type)
1202 with pull_request.set_state(PullRequest.STATE_UPDATING):
1213 with pull_request.set_state(PullRequest.STATE_UPDATING):
1203 self._merge_pull_request(
1214 self._merge_pull_request(
1204 pull_request, self._rhodecode_db_user, extras)
1215 pull_request, self._rhodecode_db_user, extras)
1205 else:
1216 else:
1206 log.debug("Pre-conditions failed, NOT merging.")
1217 log.debug("Pre-conditions failed, NOT merging.")
1207
1218
1208 raise HTTPFound(
1219 raise HTTPFound(
1209 h.route_path('pullrequest_show',
1220 h.route_path('pullrequest_show',
1210 repo_name=pull_request.target_repo.repo_name,
1221 repo_name=pull_request.target_repo.repo_name,
1211 pull_request_id=pull_request.pull_request_id))
1222 pull_request_id=pull_request.pull_request_id))
1212
1223
1213 def _merge_pull_request(self, pull_request, user, extras):
1224 def _merge_pull_request(self, pull_request, user, extras):
1214 _ = self.request.translate
1225 _ = self.request.translate
1215 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1226 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1216
1227
1217 if merge_resp.executed:
1228 if merge_resp.executed:
1218 log.debug("The merge was successful, closing the pull request.")
1229 log.debug("The merge was successful, closing the pull request.")
1219 PullRequestModel().close_pull_request(
1230 PullRequestModel().close_pull_request(
1220 pull_request.pull_request_id, user)
1231 pull_request.pull_request_id, user)
1221 Session().commit()
1232 Session().commit()
1222 msg = _('Pull request was successfully merged and closed.')
1233 msg = _('Pull request was successfully merged and closed.')
1223 h.flash(msg, category='success')
1234 h.flash(msg, category='success')
1224 else:
1235 else:
1225 log.debug(
1236 log.debug(
1226 "The merge was not successful. Merge response: %s", merge_resp)
1237 "The merge was not successful. Merge response: %s", merge_resp)
1227 msg = merge_resp.merge_status_message
1238 msg = merge_resp.merge_status_message
1228 h.flash(msg, category='error')
1239 h.flash(msg, category='error')
1229
1240
1230 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1241 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1231 _ = self.request.translate
1242 _ = self.request.translate
1232
1243
1233 get_default_reviewers_data, validate_default_reviewers = \
1244 get_default_reviewers_data, validate_default_reviewers = \
1234 PullRequestModel().get_reviewer_functions()
1245 PullRequestModel().get_reviewer_functions()
1235
1246
1236 try:
1247 try:
1237 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1248 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1238 except ValueError as e:
1249 except ValueError as e:
1239 log.error('Reviewers Validation: {}'.format(e))
1250 log.error('Reviewers Validation: {}'.format(e))
1240 h.flash(e, category='error')
1251 h.flash(e, category='error')
1241 return
1252 return
1242
1253
1243 old_calculated_status = pull_request.calculated_review_status()
1254 old_calculated_status = pull_request.calculated_review_status()
1244 PullRequestModel().update_reviewers(
1255 PullRequestModel().update_reviewers(
1245 pull_request, reviewers, self._rhodecode_user)
1256 pull_request, reviewers, self._rhodecode_user)
1246 h.flash(_('Pull request reviewers updated.'), category='success')
1257 h.flash(_('Pull request reviewers updated.'), category='success')
1247 Session().commit()
1258 Session().commit()
1248
1259
1249 # trigger status changed if change in reviewers changes the status
1260 # trigger status changed if change in reviewers changes the status
1250 calculated_status = pull_request.calculated_review_status()
1261 calculated_status = pull_request.calculated_review_status()
1251 if old_calculated_status != calculated_status:
1262 if old_calculated_status != calculated_status:
1252 PullRequestModel().trigger_pull_request_hook(
1263 PullRequestModel().trigger_pull_request_hook(
1253 pull_request, self._rhodecode_user, 'review_status_change',
1264 pull_request, self._rhodecode_user, 'review_status_change',
1254 data={'status': calculated_status})
1265 data={'status': calculated_status})
1255
1266
1256 @LoginRequired()
1267 @LoginRequired()
1257 @NotAnonymous()
1268 @NotAnonymous()
1258 @HasRepoPermissionAnyDecorator(
1269 @HasRepoPermissionAnyDecorator(
1259 'repository.read', 'repository.write', 'repository.admin')
1270 'repository.read', 'repository.write', 'repository.admin')
1260 @CSRFRequired()
1271 @CSRFRequired()
1261 @view_config(
1272 @view_config(
1262 route_name='pullrequest_delete', request_method='POST',
1273 route_name='pullrequest_delete', request_method='POST',
1263 renderer='json_ext')
1274 renderer='json_ext')
1264 def pull_request_delete(self):
1275 def pull_request_delete(self):
1265 _ = self.request.translate
1276 _ = self.request.translate
1266
1277
1267 pull_request = PullRequest.get_or_404(
1278 pull_request = PullRequest.get_or_404(
1268 self.request.matchdict['pull_request_id'])
1279 self.request.matchdict['pull_request_id'])
1269 self.load_default_context()
1280 self.load_default_context()
1270
1281
1271 pr_closed = pull_request.is_closed()
1282 pr_closed = pull_request.is_closed()
1272 allowed_to_delete = PullRequestModel().check_user_delete(
1283 allowed_to_delete = PullRequestModel().check_user_delete(
1273 pull_request, self._rhodecode_user) and not pr_closed
1284 pull_request, self._rhodecode_user) and not pr_closed
1274
1285
1275 # only owner can delete it !
1286 # only owner can delete it !
1276 if allowed_to_delete:
1287 if allowed_to_delete:
1277 PullRequestModel().delete(pull_request, self._rhodecode_user)
1288 PullRequestModel().delete(pull_request, self._rhodecode_user)
1278 Session().commit()
1289 Session().commit()
1279 h.flash(_('Successfully deleted pull request'),
1290 h.flash(_('Successfully deleted pull request'),
1280 category='success')
1291 category='success')
1281 raise HTTPFound(h.route_path('pullrequest_show_all',
1292 raise HTTPFound(h.route_path('pullrequest_show_all',
1282 repo_name=self.db_repo_name))
1293 repo_name=self.db_repo_name))
1283
1294
1284 log.warning('user %s tried to delete pull request without access',
1295 log.warning('user %s tried to delete pull request without access',
1285 self._rhodecode_user)
1296 self._rhodecode_user)
1286 raise HTTPNotFound()
1297 raise HTTPNotFound()
1287
1298
1288 @LoginRequired()
1299 @LoginRequired()
1289 @NotAnonymous()
1300 @NotAnonymous()
1290 @HasRepoPermissionAnyDecorator(
1301 @HasRepoPermissionAnyDecorator(
1291 'repository.read', 'repository.write', 'repository.admin')
1302 'repository.read', 'repository.write', 'repository.admin')
1292 @CSRFRequired()
1303 @CSRFRequired()
1293 @view_config(
1304 @view_config(
1294 route_name='pullrequest_comment_create', request_method='POST',
1305 route_name='pullrequest_comment_create', request_method='POST',
1295 renderer='json_ext')
1306 renderer='json_ext')
1296 def pull_request_comment_create(self):
1307 def pull_request_comment_create(self):
1297 _ = self.request.translate
1308 _ = self.request.translate
1298
1309
1299 pull_request = PullRequest.get_or_404(
1310 pull_request = PullRequest.get_or_404(
1300 self.request.matchdict['pull_request_id'])
1311 self.request.matchdict['pull_request_id'])
1301 pull_request_id = pull_request.pull_request_id
1312 pull_request_id = pull_request.pull_request_id
1302
1313
1303 if pull_request.is_closed():
1314 if pull_request.is_closed():
1304 log.debug('comment: forbidden because pull request is closed')
1315 log.debug('comment: forbidden because pull request is closed')
1305 raise HTTPForbidden()
1316 raise HTTPForbidden()
1306
1317
1307 allowed_to_comment = PullRequestModel().check_user_comment(
1318 allowed_to_comment = PullRequestModel().check_user_comment(
1308 pull_request, self._rhodecode_user)
1319 pull_request, self._rhodecode_user)
1309 if not allowed_to_comment:
1320 if not allowed_to_comment:
1310 log.debug(
1321 log.debug(
1311 'comment: forbidden because pull request is from forbidden repo')
1322 'comment: forbidden because pull request is from forbidden repo')
1312 raise HTTPForbidden()
1323 raise HTTPForbidden()
1313
1324
1314 c = self.load_default_context()
1325 c = self.load_default_context()
1315
1326
1316 status = self.request.POST.get('changeset_status', None)
1327 status = self.request.POST.get('changeset_status', None)
1317 text = self.request.POST.get('text')
1328 text = self.request.POST.get('text')
1318 comment_type = self.request.POST.get('comment_type')
1329 comment_type = self.request.POST.get('comment_type')
1319 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1330 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1320 close_pull_request = self.request.POST.get('close_pull_request')
1331 close_pull_request = self.request.POST.get('close_pull_request')
1321
1332
1322 # the logic here should work like following, if we submit close
1333 # the logic here should work like following, if we submit close
1323 # pr comment, use `close_pull_request_with_comment` function
1334 # pr comment, use `close_pull_request_with_comment` function
1324 # else handle regular comment logic
1335 # else handle regular comment logic
1325
1336
1326 if close_pull_request:
1337 if close_pull_request:
1327 # only owner or admin or person with write permissions
1338 # only owner or admin or person with write permissions
1328 allowed_to_close = PullRequestModel().check_user_update(
1339 allowed_to_close = PullRequestModel().check_user_update(
1329 pull_request, self._rhodecode_user)
1340 pull_request, self._rhodecode_user)
1330 if not allowed_to_close:
1341 if not allowed_to_close:
1331 log.debug('comment: forbidden because not allowed to close '
1342 log.debug('comment: forbidden because not allowed to close '
1332 'pull request %s', pull_request_id)
1343 'pull request %s', pull_request_id)
1333 raise HTTPForbidden()
1344 raise HTTPForbidden()
1334
1345
1335 # This also triggers `review_status_change`
1346 # This also triggers `review_status_change`
1336 comment, status = PullRequestModel().close_pull_request_with_comment(
1347 comment, status = PullRequestModel().close_pull_request_with_comment(
1337 pull_request, self._rhodecode_user, self.db_repo, message=text,
1348 pull_request, self._rhodecode_user, self.db_repo, message=text,
1338 auth_user=self._rhodecode_user)
1349 auth_user=self._rhodecode_user)
1339 Session().flush()
1350 Session().flush()
1340
1351
1341 PullRequestModel().trigger_pull_request_hook(
1352 PullRequestModel().trigger_pull_request_hook(
1342 pull_request, self._rhodecode_user, 'comment',
1353 pull_request, self._rhodecode_user, 'comment',
1343 data={'comment': comment})
1354 data={'comment': comment})
1344
1355
1345 else:
1356 else:
1346 # regular comment case, could be inline, or one with status.
1357 # regular comment case, could be inline, or one with status.
1347 # for that one we check also permissions
1358 # for that one we check also permissions
1348
1359
1349 allowed_to_change_status = PullRequestModel().check_user_change_status(
1360 allowed_to_change_status = PullRequestModel().check_user_change_status(
1350 pull_request, self._rhodecode_user)
1361 pull_request, self._rhodecode_user)
1351
1362
1352 if status and allowed_to_change_status:
1363 if status and allowed_to_change_status:
1353 message = (_('Status change %(transition_icon)s %(status)s')
1364 message = (_('Status change %(transition_icon)s %(status)s')
1354 % {'transition_icon': '>',
1365 % {'transition_icon': '>',
1355 'status': ChangesetStatus.get_status_lbl(status)})
1366 'status': ChangesetStatus.get_status_lbl(status)})
1356 text = text or message
1367 text = text or message
1357
1368
1358 comment = CommentsModel().create(
1369 comment = CommentsModel().create(
1359 text=text,
1370 text=text,
1360 repo=self.db_repo.repo_id,
1371 repo=self.db_repo.repo_id,
1361 user=self._rhodecode_user.user_id,
1372 user=self._rhodecode_user.user_id,
1362 pull_request=pull_request,
1373 pull_request=pull_request,
1363 f_path=self.request.POST.get('f_path'),
1374 f_path=self.request.POST.get('f_path'),
1364 line_no=self.request.POST.get('line'),
1375 line_no=self.request.POST.get('line'),
1365 status_change=(ChangesetStatus.get_status_lbl(status)
1376 status_change=(ChangesetStatus.get_status_lbl(status)
1366 if status and allowed_to_change_status else None),
1377 if status and allowed_to_change_status else None),
1367 status_change_type=(status
1378 status_change_type=(status
1368 if status and allowed_to_change_status else None),
1379 if status and allowed_to_change_status else None),
1369 comment_type=comment_type,
1380 comment_type=comment_type,
1370 resolves_comment_id=resolves_comment_id,
1381 resolves_comment_id=resolves_comment_id,
1371 auth_user=self._rhodecode_user
1382 auth_user=self._rhodecode_user
1372 )
1383 )
1373
1384
1374 if allowed_to_change_status:
1385 if allowed_to_change_status:
1375 # calculate old status before we change it
1386 # calculate old status before we change it
1376 old_calculated_status = pull_request.calculated_review_status()
1387 old_calculated_status = pull_request.calculated_review_status()
1377
1388
1378 # get status if set !
1389 # get status if set !
1379 if status:
1390 if status:
1380 ChangesetStatusModel().set_status(
1391 ChangesetStatusModel().set_status(
1381 self.db_repo.repo_id,
1392 self.db_repo.repo_id,
1382 status,
1393 status,
1383 self._rhodecode_user.user_id,
1394 self._rhodecode_user.user_id,
1384 comment,
1395 comment,
1385 pull_request=pull_request
1396 pull_request=pull_request
1386 )
1397 )
1387
1398
1388 Session().flush()
1399 Session().flush()
1389 # this is somehow required to get access to some relationship
1400 # this is somehow required to get access to some relationship
1390 # loaded on comment
1401 # loaded on comment
1391 Session().refresh(comment)
1402 Session().refresh(comment)
1392
1403
1393 PullRequestModel().trigger_pull_request_hook(
1404 PullRequestModel().trigger_pull_request_hook(
1394 pull_request, self._rhodecode_user, 'comment',
1405 pull_request, self._rhodecode_user, 'comment',
1395 data={'comment': comment})
1406 data={'comment': comment})
1396
1407
1397 # we now calculate the status of pull request, and based on that
1408 # we now calculate the status of pull request, and based on that
1398 # calculation we set the commits status
1409 # calculation we set the commits status
1399 calculated_status = pull_request.calculated_review_status()
1410 calculated_status = pull_request.calculated_review_status()
1400 if old_calculated_status != calculated_status:
1411 if old_calculated_status != calculated_status:
1401 PullRequestModel().trigger_pull_request_hook(
1412 PullRequestModel().trigger_pull_request_hook(
1402 pull_request, self._rhodecode_user, 'review_status_change',
1413 pull_request, self._rhodecode_user, 'review_status_change',
1403 data={'status': calculated_status})
1414 data={'status': calculated_status})
1404
1415
1405 Session().commit()
1416 Session().commit()
1406
1417
1407 data = {
1418 data = {
1408 'target_id': h.safeid(h.safe_unicode(
1419 'target_id': h.safeid(h.safe_unicode(
1409 self.request.POST.get('f_path'))),
1420 self.request.POST.get('f_path'))),
1410 }
1421 }
1411 if comment:
1422 if comment:
1412 c.co = comment
1423 c.co = comment
1413 rendered_comment = render(
1424 rendered_comment = render(
1414 'rhodecode:templates/changeset/changeset_comment_block.mako',
1425 'rhodecode:templates/changeset/changeset_comment_block.mako',
1415 self._get_template_context(c), self.request)
1426 self._get_template_context(c), self.request)
1416
1427
1417 data.update(comment.get_dict())
1428 data.update(comment.get_dict())
1418 data.update({'rendered_text': rendered_comment})
1429 data.update({'rendered_text': rendered_comment})
1419
1430
1420 return data
1431 return data
1421
1432
1422 @LoginRequired()
1433 @LoginRequired()
1423 @NotAnonymous()
1434 @NotAnonymous()
1424 @HasRepoPermissionAnyDecorator(
1435 @HasRepoPermissionAnyDecorator(
1425 'repository.read', 'repository.write', 'repository.admin')
1436 'repository.read', 'repository.write', 'repository.admin')
1426 @CSRFRequired()
1437 @CSRFRequired()
1427 @view_config(
1438 @view_config(
1428 route_name='pullrequest_comment_delete', request_method='POST',
1439 route_name='pullrequest_comment_delete', request_method='POST',
1429 renderer='json_ext')
1440 renderer='json_ext')
1430 def pull_request_comment_delete(self):
1441 def pull_request_comment_delete(self):
1431 pull_request = PullRequest.get_or_404(
1442 pull_request = PullRequest.get_or_404(
1432 self.request.matchdict['pull_request_id'])
1443 self.request.matchdict['pull_request_id'])
1433
1444
1434 comment = ChangesetComment.get_or_404(
1445 comment = ChangesetComment.get_or_404(
1435 self.request.matchdict['comment_id'])
1446 self.request.matchdict['comment_id'])
1436 comment_id = comment.comment_id
1447 comment_id = comment.comment_id
1437
1448
1438 if pull_request.is_closed():
1449 if pull_request.is_closed():
1439 log.debug('comment: forbidden because pull request is closed')
1450 log.debug('comment: forbidden because pull request is closed')
1440 raise HTTPForbidden()
1451 raise HTTPForbidden()
1441
1452
1442 if not comment:
1453 if not comment:
1443 log.debug('Comment with id:%s not found, skipping', comment_id)
1454 log.debug('Comment with id:%s not found, skipping', comment_id)
1444 # comment already deleted in another call probably
1455 # comment already deleted in another call probably
1445 return True
1456 return True
1446
1457
1447 if comment.pull_request.is_closed():
1458 if comment.pull_request.is_closed():
1448 # don't allow deleting comments on closed pull request
1459 # don't allow deleting comments on closed pull request
1449 raise HTTPForbidden()
1460 raise HTTPForbidden()
1450
1461
1451 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1462 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1452 super_admin = h.HasPermissionAny('hg.admin')()
1463 super_admin = h.HasPermissionAny('hg.admin')()
1453 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1464 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1454 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1465 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1455 comment_repo_admin = is_repo_admin and is_repo_comment
1466 comment_repo_admin = is_repo_admin and is_repo_comment
1456
1467
1457 if super_admin or comment_owner or comment_repo_admin:
1468 if super_admin or comment_owner or comment_repo_admin:
1458 old_calculated_status = comment.pull_request.calculated_review_status()
1469 old_calculated_status = comment.pull_request.calculated_review_status()
1459 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1470 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1460 Session().commit()
1471 Session().commit()
1461 calculated_status = comment.pull_request.calculated_review_status()
1472 calculated_status = comment.pull_request.calculated_review_status()
1462 if old_calculated_status != calculated_status:
1473 if old_calculated_status != calculated_status:
1463 PullRequestModel().trigger_pull_request_hook(
1474 PullRequestModel().trigger_pull_request_hook(
1464 comment.pull_request, self._rhodecode_user, 'review_status_change',
1475 comment.pull_request, self._rhodecode_user, 'review_status_change',
1465 data={'status': calculated_status})
1476 data={'status': calculated_status})
1466 return True
1477 return True
1467 else:
1478 else:
1468 log.warning('No permissions for user %s to delete comment_id: %s',
1479 log.warning('No permissions for user %s to delete comment_id: %s',
1469 self._rhodecode_db_user, comment_id)
1480 self._rhodecode_db_user, comment_id)
1470 raise HTTPNotFound()
1481 raise HTTPNotFound()
@@ -1,475 +1,523 b''
1
1
2
2
3 //BUTTONS
3 //BUTTONS
4 button,
4 button,
5 .btn,
5 .btn,
6 input[type="button"] {
6 input[type="button"] {
7 -webkit-appearance: none;
7 -webkit-appearance: none;
8 display: inline-block;
8 display: inline-block;
9 margin: 0 @padding/3 0 0;
9 margin: 0 @padding/3 0 0;
10 padding: @button-padding;
10 padding: @button-padding;
11 text-align: center;
11 text-align: center;
12 font-size: @basefontsize;
12 font-size: @basefontsize;
13 line-height: 1em;
13 line-height: 1em;
14 font-family: @text-light;
14 font-family: @text-light;
15 text-decoration: none;
15 text-decoration: none;
16 text-shadow: none;
16 text-shadow: none;
17 color: @grey2;
17 color: @grey2;
18 background-color: white;
18 background-color: white;
19 background-image: none;
19 background-image: none;
20 border: none;
20 border: none;
21 .border ( @border-thickness-buttons, @grey5 );
21 .border ( @border-thickness-buttons, @grey5 );
22 .border-radius (@border-radius);
22 .border-radius (@border-radius);
23 cursor: pointer;
23 cursor: pointer;
24 white-space: nowrap;
24 white-space: nowrap;
25 -webkit-transition: background .3s,color .3s;
25 -webkit-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
29 box-shadow: @button-shadow;
29 box-shadow: @button-shadow;
30 -webkit-box-shadow: @button-shadow;
30 -webkit-box-shadow: @button-shadow;
31
31
32
32
33
33
34 a {
34 a {
35 display: block;
35 display: block;
36 margin: 0;
36 margin: 0;
37 padding: 0;
37 padding: 0;
38 color: inherit;
38 color: inherit;
39 text-decoration: none;
39 text-decoration: none;
40
40
41 &:hover {
41 &:hover {
42 text-decoration: none;
42 text-decoration: none;
43 }
43 }
44 }
44 }
45
45
46 &:focus,
46 &:focus,
47 &:active {
47 &:active {
48 outline:none;
48 outline:none;
49 }
49 }
50 &:hover {
50 &:hover {
51 color: @rcdarkblue;
51 color: @rcdarkblue;
52 background-color: @white;
52 background-color: @white;
53 .border ( @border-thickness, @grey4 );
53 .border ( @border-thickness, @grey4 );
54 }
54 }
55
55
56 .icon-remove {
56 .icon-remove {
57 display: none;
57 display: none;
58 }
58 }
59
59
60 //disabled buttons
60 //disabled buttons
61 //last; overrides any other styles
61 //last; overrides any other styles
62 &:disabled {
62 &:disabled {
63 opacity: .7;
63 opacity: .7;
64 cursor: auto;
64 cursor: auto;
65 background-color: white;
65 background-color: white;
66 color: @grey4;
66 color: @grey4;
67 text-shadow: none;
67 text-shadow: none;
68 }
68 }
69
69
70 &.no-margin {
70 &.no-margin {
71 margin: 0 0 0 0;
71 margin: 0 0 0 0;
72 }
72 }
73
73
74 &.btn-active {
74 &.btn-active {
75 color: @rcdarkblue;
75 color: @rcdarkblue;
76 background-color: @white;
76 background-color: @white;
77 .border ( @border-thickness, @rcdarkblue );
77 .border ( @border-thickness, @rcdarkblue );
78 }
78 }
79
79
80 }
80 }
81
81
82
82
83 .btn-default {
83 .btn-default {
84 border: @border-thickness solid @grey5;
84 border: @border-thickness solid @grey5;
85 background-image: none;
85 background-image: none;
86 color: @grey2;
86 color: @grey2;
87
87
88 a {
88 a {
89 color: @grey2;
89 color: @grey2;
90 }
90 }
91
91
92 &:hover,
92 &:hover,
93 &.active {
93 &.active {
94 color: @rcdarkblue;
94 color: @rcdarkblue;
95 background-color: @white;
95 background-color: @white;
96 .border ( @border-thickness, @grey4 );
96 .border ( @border-thickness, @grey4 );
97
97
98 a {
98 a {
99 color: @grey2;
99 color: @grey2;
100 }
100 }
101 }
101 }
102 &:disabled {
102 &:disabled {
103 .border ( @border-thickness-buttons, @grey5 );
103 .border ( @border-thickness-buttons, @grey5 );
104 background-color: transparent;
104 background-color: transparent;
105 }
105 }
106 &.btn-active {
106 &.btn-active {
107 color: @rcdarkblue;
107 color: @rcdarkblue;
108 background-color: @white;
108 background-color: @white;
109 .border ( @border-thickness, @rcdarkblue );
109 .border ( @border-thickness, @rcdarkblue );
110 }
110 }
111 }
111 }
112
112
113 .btn-primary,
113 .btn-primary,
114 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
114 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
115 .btn-success {
115 .btn-success {
116 .border ( @border-thickness, @rcblue );
116 .border ( @border-thickness, @rcblue );
117 background-color: @rcblue;
117 background-color: @rcblue;
118 color: white;
118 color: white;
119
119
120 a {
120 a {
121 color: white;
121 color: white;
122 }
122 }
123
123
124 &:hover,
124 &:hover,
125 &.active {
125 &.active {
126 .border ( @border-thickness, @rcdarkblue );
126 .border ( @border-thickness, @rcdarkblue );
127 color: white;
127 color: white;
128 background-color: @rcdarkblue;
128 background-color: @rcdarkblue;
129
129
130 a {
130 a {
131 color: white;
131 color: white;
132 }
132 }
133 }
133 }
134 &:disabled {
134 &:disabled {
135 background-color: @rcblue;
135 background-color: @rcblue;
136 }
136 }
137 }
137 }
138
138
139 .btn-secondary {
139 .btn-secondary {
140 &:extend(.btn-default);
140 &:extend(.btn-default);
141
141
142 background-color: white;
142 background-color: white;
143
143
144 &:focus {
144 &:focus {
145 outline: 0;
145 outline: 0;
146 }
146 }
147
147
148 &:hover {
148 &:hover {
149 &:extend(.btn-default:hover);
149 &:extend(.btn-default:hover);
150 }
150 }
151
151
152 &.btn-link {
152 &.btn-link {
153 &:extend(.btn-link);
153 &:extend(.btn-link);
154 color: @rcblue;
154 color: @rcblue;
155 }
155 }
156
156
157 &:disabled {
157 &:disabled {
158 color: @rcblue;
158 color: @rcblue;
159 background-color: white;
159 background-color: white;
160 }
160 }
161 }
161 }
162
162
163 .btn-warning,
163 .btn-warning,
164 .btn-danger,
164 .btn-danger,
165 .revoke_perm,
165 .revoke_perm,
166 .btn-x,
166 .btn-x,
167 .form .action_button.btn-x {
167 .form .action_button.btn-x {
168 .border ( @border-thickness, @alert2 );
168 .border ( @border-thickness, @alert2 );
169 background-color: white;
169 background-color: white;
170 color: @alert2;
170 color: @alert2;
171
171
172 a {
172 a {
173 color: @alert2;
173 color: @alert2;
174 }
174 }
175
175
176 &:hover,
176 &:hover,
177 &.active {
177 &.active {
178 .border ( @border-thickness, @alert2 );
178 .border ( @border-thickness, @alert2 );
179 color: white;
179 color: white;
180 background-color: @alert2;
180 background-color: @alert2;
181
181
182 a {
182 a {
183 color: white;
183 color: white;
184 }
184 }
185 }
185 }
186
186
187 i {
187 i {
188 display:none;
188 display:none;
189 }
189 }
190
190
191 &:disabled {
191 &:disabled {
192 background-color: white;
192 background-color: white;
193 color: @alert2;
193 color: @alert2;
194 }
194 }
195 }
195 }
196
196
197 .btn-approved-status {
197 .btn-approved-status {
198 .border ( @border-thickness, @alert1 );
198 .border ( @border-thickness, @alert1 );
199 background-color: white;
199 background-color: white;
200 color: @alert1;
200 color: @alert1;
201
201
202 }
202 }
203
203
204 .btn-rejected-status {
204 .btn-rejected-status {
205 .border ( @border-thickness, @alert2 );
205 .border ( @border-thickness, @alert2 );
206 background-color: white;
206 background-color: white;
207 color: @alert2;
207 color: @alert2;
208 }
208 }
209
209
210 .btn-sm,
210 .btn-sm,
211 .btn-mini,
211 .btn-mini,
212 .field-sm .btn {
212 .field-sm .btn {
213 padding: @padding/3;
213 padding: @padding/3;
214 }
214 }
215
215
216 .btn-xs {
216 .btn-xs {
217 padding: @padding/4;
217 padding: @padding/4;
218 }
218 }
219
219
220 .btn-lg {
220 .btn-lg {
221 padding: @padding * 1.2;
221 padding: @padding * 1.2;
222 }
222 }
223
223
224 .btn-group {
224 .btn-group {
225 display: inline-block;
225 display: inline-block;
226 .btn {
226 .btn {
227 float: left;
227 float: left;
228 margin: 0 0 0 0;
228 margin: 0 0 0 0;
229 // first item
229 // first item
230 &:first-of-type:not(:last-of-type) {
230 &:first-of-type:not(:last-of-type) {
231 border-radius: @border-radius 0 0 @border-radius;
231 border-radius: @border-radius 0 0 @border-radius;
232
232
233 }
233 }
234 // middle elements
234 // middle elements
235 &:not(:first-of-type):not(:last-of-type) {
235 &:not(:first-of-type):not(:last-of-type) {
236 border-radius: 0;
236 border-radius: 0;
237 border-left-width: 0;
237 border-left-width: 0;
238 border-right-width: 0;
238 border-right-width: 0;
239 }
239 }
240 // last item
240 // last item
241 &:last-of-type:not(:first-of-type) {
241 &:last-of-type:not(:first-of-type) {
242 border-radius: 0 @border-radius @border-radius 0;
242 border-radius: 0 @border-radius @border-radius 0;
243 }
243 }
244
244
245 &:only-child {
245 &:only-child {
246 border-radius: @border-radius;
246 border-radius: @border-radius;
247 }
247 }
248 }
248 }
249
249
250 }
250 }
251
251
252
253 .btn-group-actions {
254 position: relative;
255 z-index: 100;
256
257 &:not(.open) .btn-action-switcher-container {
258 display: none;
259 }
260 }
261
262
263 .btn-action-switcher-container{
264 position: absolute;
265 top: 30px;
266 left: 0px;
267 }
268
269 .btn-action-switcher {
270 display: block;
271 position: relative;
272 z-index: 300;
273 min-width: 240px;
274 max-width: 500px;
275 margin-top: 4px;
276 margin-bottom: 24px;
277 font-size: 14px;
278 font-weight: 400;
279 padding: 8px 0;
280 background-color: #fff;
281 border: 1px solid @grey4;
282 border-radius: 3px;
283 box-shadow: @dropdown-shadow;
284
285 li {
286 display: block;
287 text-align: left;
288 list-style: none;
289 padding: 5px 10px;
290 }
291
292 li .action-help-block {
293 font-size: 10px;
294 line-height: normal;
295 color: @grey4;
296 }
297
298 }
299
252 .btn-link {
300 .btn-link {
253 background: transparent;
301 background: transparent;
254 border: none;
302 border: none;
255 padding: 0;
303 padding: 0;
256 color: @rcblue;
304 color: @rcblue;
257
305
258 &:hover {
306 &:hover {
259 background: transparent;
307 background: transparent;
260 border: none;
308 border: none;
261 color: @rcdarkblue;
309 color: @rcdarkblue;
262 }
310 }
263
311
264 //disabled buttons
312 //disabled buttons
265 //last; overrides any other styles
313 //last; overrides any other styles
266 &:disabled {
314 &:disabled {
267 opacity: .7;
315 opacity: .7;
268 cursor: auto;
316 cursor: auto;
269 background-color: white;
317 background-color: white;
270 color: @grey4;
318 color: @grey4;
271 text-shadow: none;
319 text-shadow: none;
272 }
320 }
273
321
274 // TODO: johbo: Check if we can avoid this, indicates that the structure
322 // TODO: johbo: Check if we can avoid this, indicates that the structure
275 // is not yet good.
323 // is not yet good.
276 // lisa: The button CSS reflects the button HTML; both need a cleanup.
324 // lisa: The button CSS reflects the button HTML; both need a cleanup.
277 &.btn-danger {
325 &.btn-danger {
278 color: @alert2;
326 color: @alert2;
279
327
280 &:hover {
328 &:hover {
281 color: darken(@alert2,30%);
329 color: darken(@alert2,30%);
282 }
330 }
283
331
284 &:disabled {
332 &:disabled {
285 color: @alert2;
333 color: @alert2;
286 }
334 }
287 }
335 }
288 }
336 }
289
337
290 .btn-social {
338 .btn-social {
291 &:extend(.btn-default);
339 &:extend(.btn-default);
292 margin: 5px 5px 5px 0px;
340 margin: 5px 5px 5px 0px;
293 min-width: 160px;
341 min-width: 160px;
294 }
342 }
295
343
296 // TODO: johbo: check these exceptions
344 // TODO: johbo: check these exceptions
297
345
298 .links {
346 .links {
299
347
300 .btn + .btn {
348 .btn + .btn {
301 margin-top: @padding;
349 margin-top: @padding;
302 }
350 }
303 }
351 }
304
352
305
353
306 .action_button {
354 .action_button {
307 display:inline;
355 display:inline;
308 margin: 0;
356 margin: 0;
309 padding: 0 1em 0 0;
357 padding: 0 1em 0 0;
310 font-size: inherit;
358 font-size: inherit;
311 color: @rcblue;
359 color: @rcblue;
312 border: none;
360 border: none;
313 border-radius: 0;
361 border-radius: 0;
314 background-color: transparent;
362 background-color: transparent;
315
363
316 &.last-item {
364 &.last-item {
317 border: none;
365 border: none;
318 padding: 0 0 0 0;
366 padding: 0 0 0 0;
319 }
367 }
320
368
321 &:last-child {
369 &:last-child {
322 border: none;
370 border: none;
323 padding: 0 0 0 0;
371 padding: 0 0 0 0;
324 }
372 }
325
373
326 &:hover {
374 &:hover {
327 color: @rcdarkblue;
375 color: @rcdarkblue;
328 background-color: transparent;
376 background-color: transparent;
329 border: none;
377 border: none;
330 }
378 }
331 }
379 }
332 .grid_delete {
380 .grid_delete {
333 .action_button {
381 .action_button {
334 border: none;
382 border: none;
335 }
383 }
336 }
384 }
337
385
338
386
339 // TODO: johbo: Form button tweaks, check if we can use the classes instead
387 // TODO: johbo: Form button tweaks, check if we can use the classes instead
340 input[type="submit"] {
388 input[type="submit"] {
341 &:extend(.btn-primary);
389 &:extend(.btn-primary);
342
390
343 &:focus {
391 &:focus {
344 outline: 0;
392 outline: 0;
345 }
393 }
346
394
347 &:hover {
395 &:hover {
348 &:extend(.btn-primary:hover);
396 &:extend(.btn-primary:hover);
349 }
397 }
350
398
351 &.btn-link {
399 &.btn-link {
352 &:extend(.btn-link);
400 &:extend(.btn-link);
353 color: @rcblue;
401 color: @rcblue;
354
402
355 &:disabled {
403 &:disabled {
356 color: @rcblue;
404 color: @rcblue;
357 background-color: transparent;
405 background-color: transparent;
358 }
406 }
359 }
407 }
360
408
361 &:disabled {
409 &:disabled {
362 .border ( @border-thickness-buttons, @rcblue );
410 .border ( @border-thickness-buttons, @rcblue );
363 background-color: @rcblue;
411 background-color: @rcblue;
364 color: white;
412 color: white;
365 opacity: 0.5;
413 opacity: 0.5;
366 }
414 }
367 }
415 }
368
416
369 input[type="reset"] {
417 input[type="reset"] {
370 &:extend(.btn-default);
418 &:extend(.btn-default);
371
419
372 // TODO: johbo: Check if this tweak can be avoided.
420 // TODO: johbo: Check if this tweak can be avoided.
373 background: transparent;
421 background: transparent;
374
422
375 &:focus {
423 &:focus {
376 outline: 0;
424 outline: 0;
377 }
425 }
378
426
379 &:hover {
427 &:hover {
380 &:extend(.btn-default:hover);
428 &:extend(.btn-default:hover);
381 }
429 }
382
430
383 &.btn-link {
431 &.btn-link {
384 &:extend(.btn-link);
432 &:extend(.btn-link);
385 color: @rcblue;
433 color: @rcblue;
386
434
387 &:disabled {
435 &:disabled {
388 border: none;
436 border: none;
389 }
437 }
390 }
438 }
391
439
392 &:disabled {
440 &:disabled {
393 .border ( @border-thickness-buttons, @rcblue );
441 .border ( @border-thickness-buttons, @rcblue );
394 background-color: white;
442 background-color: white;
395 color: @rcblue;
443 color: @rcblue;
396 }
444 }
397 }
445 }
398
446
399 input[type="submit"],
447 input[type="submit"],
400 input[type="reset"] {
448 input[type="reset"] {
401 &.btn-danger {
449 &.btn-danger {
402 &:extend(.btn-danger);
450 &:extend(.btn-danger);
403
451
404 &:focus {
452 &:focus {
405 outline: 0;
453 outline: 0;
406 }
454 }
407
455
408 &:hover {
456 &:hover {
409 &:extend(.btn-danger:hover);
457 &:extend(.btn-danger:hover);
410 }
458 }
411
459
412 &.btn-link {
460 &.btn-link {
413 &:extend(.btn-link);
461 &:extend(.btn-link);
414 color: @alert2;
462 color: @alert2;
415
463
416 &:hover {
464 &:hover {
417 color: darken(@alert2,30%);
465 color: darken(@alert2,30%);
418 }
466 }
419 }
467 }
420
468
421 &:disabled {
469 &:disabled {
422 color: @alert2;
470 color: @alert2;
423 background-color: white;
471 background-color: white;
424 }
472 }
425 }
473 }
426 &.btn-danger-action {
474 &.btn-danger-action {
427 .border ( @border-thickness, @alert2 );
475 .border ( @border-thickness, @alert2 );
428 background-color: @alert2;
476 background-color: @alert2;
429 color: white;
477 color: white;
430
478
431 a {
479 a {
432 color: white;
480 color: white;
433 }
481 }
434
482
435 &:hover {
483 &:hover {
436 background-color: darken(@alert2,20%);
484 background-color: darken(@alert2,20%);
437 }
485 }
438
486
439 &.active {
487 &.active {
440 .border ( @border-thickness, @alert2 );
488 .border ( @border-thickness, @alert2 );
441 color: white;
489 color: white;
442 background-color: @alert2;
490 background-color: @alert2;
443
491
444 a {
492 a {
445 color: white;
493 color: white;
446 }
494 }
447 }
495 }
448
496
449 &:disabled {
497 &:disabled {
450 background-color: white;
498 background-color: white;
451 color: @alert2;
499 color: @alert2;
452 }
500 }
453 }
501 }
454 }
502 }
455
503
456
504
457 .button-links {
505 .button-links {
458 float: left;
506 float: left;
459 display: inline;
507 display: inline;
460 margin: 0;
508 margin: 0;
461 padding-left: 0;
509 padding-left: 0;
462 list-style: none;
510 list-style: none;
463 text-align: right;
511 text-align: right;
464
512
465 li {
513 li {
466
514
467
515
468 }
516 }
469
517
470 li.active {
518 li.active {
471 background-color: @grey6;
519 background-color: @grey6;
472 .border ( @border-thickness, @grey4 );
520 .border ( @border-thickness, @grey4 );
473 }
521 }
474
522
475 }
523 }
@@ -1,552 +1,608 b''
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#pr_submit').attr('disabled', 'disabled');
44 $('#pr_submit').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#pr_submit').removeAttr('disabled');
47 $('#pr_submit').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
84 }
85 else {
85 else {
86 // use reference name
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
88 }
89
89
90 return [title, desc]
90 return [title, desc]
91 };
91 };
92
92
93
93
94
94
95 ReviewersController = function () {
95 ReviewersController = function () {
96 var self = this;
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
101 this.currentRequest = null;
102
102
103 this.defaultForbidReviewUsers = function() {
103 this.defaultForbidReviewUsers = function() {
104 return [
104 return [
105 {'username': 'default',
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
106 'user_id': templateContext.default_user.user_id}
107 ];
107 ];
108 };
108 };
109
109
110 this.hideReviewRules = function() {
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
111 self.$reviewRulesContainer.hide();
112 };
112 };
113
113
114 this.showReviewRules = function() {
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
115 self.$reviewRulesContainer.show();
116 };
116 };
117
117
118 this.addRule = function(ruleText) {
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
120 return '<div>- {0}</div>'.format(ruleText)
121 };
121 };
122
122
123 this.loadReviewRules = function(data) {
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
126
127 // reset state of review rules
127 // reset state of review rules
128 self.$rulesList.html('');
128 self.$rulesList.html('');
129
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
132 self.$rulesList.append(
133 self.addRule(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
134 _gettext('All reviewers must vote.'))
135 );
135 );
136 return self.forbidReviewUsers
136 return self.forbidReviewUsers
137 }
137 }
138
138
139 if (data.rules.voting !== undefined) {
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0) {
140 if (data.rules.voting < 0) {
141 self.$rulesList.append(
141 self.$rulesList.append(
142 self.addRule(
142 self.addRule(
143 _gettext('All individual reviewers must vote.'))
143 _gettext('All individual reviewers must vote.'))
144 )
144 )
145 } else if (data.rules.voting === 1) {
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
146 self.$rulesList.append(
147 self.addRule(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
149 )
150
150
151 } else {
151 } else {
152 self.$rulesList.append(
152 self.$rulesList.append(
153 self.addRule(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
155 )
156 }
156 }
157 }
157 }
158
158
159 if (data.rules.voting_groups !== undefined) {
159 if (data.rules.voting_groups !== undefined) {
160 $.each(data.rules.voting_groups, function(index, rule_data) {
160 $.each(data.rules.voting_groups, function(index, rule_data) {
161 self.$rulesList.append(
161 self.$rulesList.append(
162 self.addRule(rule_data.text)
162 self.addRule(rule_data.text)
163 )
163 )
164 });
164 });
165 }
165 }
166
166
167 if (data.rules.use_code_authors_for_review) {
167 if (data.rules.use_code_authors_for_review) {
168 self.$rulesList.append(
168 self.$rulesList.append(
169 self.addRule(
169 self.addRule(
170 _gettext('Reviewers picked from source code changes.'))
170 _gettext('Reviewers picked from source code changes.'))
171 )
171 )
172 }
172 }
173 if (data.rules.forbid_adding_reviewers) {
173 if (data.rules.forbid_adding_reviewers) {
174 $('#add_reviewer_input').remove();
174 $('#add_reviewer_input').remove();
175 self.$rulesList.append(
175 self.$rulesList.append(
176 self.addRule(
176 self.addRule(
177 _gettext('Adding new reviewers is forbidden.'))
177 _gettext('Adding new reviewers is forbidden.'))
178 )
178 )
179 }
179 }
180 if (data.rules.forbid_author_to_review) {
180 if (data.rules.forbid_author_to_review) {
181 self.forbidReviewUsers.push(data.rules_data.pr_author);
181 self.forbidReviewUsers.push(data.rules_data.pr_author);
182 self.$rulesList.append(
182 self.$rulesList.append(
183 self.addRule(
183 self.addRule(
184 _gettext('Author is not allowed to be a reviewer.'))
184 _gettext('Author is not allowed to be a reviewer.'))
185 )
185 )
186 }
186 }
187 if (data.rules.forbid_commit_author_to_review) {
187 if (data.rules.forbid_commit_author_to_review) {
188
188
189 if (data.rules_data.forbidden_users) {
189 if (data.rules_data.forbidden_users) {
190 $.each(data.rules_data.forbidden_users, function(index, member_data) {
190 $.each(data.rules_data.forbidden_users, function(index, member_data) {
191 self.forbidReviewUsers.push(member_data)
191 self.forbidReviewUsers.push(member_data)
192 });
192 });
193
193
194 }
194 }
195
195
196 self.$rulesList.append(
196 self.$rulesList.append(
197 self.addRule(
197 self.addRule(
198 _gettext('Commit Authors are not allowed to be a reviewer.'))
198 _gettext('Commit Authors are not allowed to be a reviewer.'))
199 )
199 )
200 }
200 }
201
201
202 return self.forbidReviewUsers
202 return self.forbidReviewUsers
203 };
203 };
204
204
205 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
205 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
206
206
207 if (self.currentRequest) {
207 if (self.currentRequest) {
208 // make sure we cleanup old running requests before triggering this
208 // make sure we cleanup old running requests before triggering this
209 // again
209 // again
210 self.currentRequest.abort();
210 self.currentRequest.abort();
211 }
211 }
212
212
213 $('.calculate-reviewers').show();
213 $('.calculate-reviewers').show();
214 // reset reviewer members
214 // reset reviewer members
215 self.$reviewMembers.empty();
215 self.$reviewMembers.empty();
216
216
217 prButtonLock(true, null, 'reviewers');
217 prButtonLock(true, null, 'reviewers');
218 $('#user').hide(); // hide user autocomplete before load
218 $('#user').hide(); // hide user autocomplete before load
219
219
220 if (sourceRef.length !== 3 || targetRef.length !== 3) {
220 if (sourceRef.length !== 3 || targetRef.length !== 3) {
221 // don't load defaults in case we're missing some refs...
221 // don't load defaults in case we're missing some refs...
222 $('.calculate-reviewers').hide();
222 $('.calculate-reviewers').hide();
223 return
223 return
224 }
224 }
225
225
226 var url = pyroutes.url('repo_default_reviewers_data',
226 var url = pyroutes.url('repo_default_reviewers_data',
227 {
227 {
228 'repo_name': templateContext.repo_name,
228 'repo_name': templateContext.repo_name,
229 'source_repo': sourceRepo,
229 'source_repo': sourceRepo,
230 'source_ref': sourceRef[2],
230 'source_ref': sourceRef[2],
231 'target_repo': targetRepo,
231 'target_repo': targetRepo,
232 'target_ref': targetRef[2]
232 'target_ref': targetRef[2]
233 });
233 });
234
234
235 self.currentRequest = $.get(url)
235 self.currentRequest = $.get(url)
236 .done(function(data) {
236 .done(function(data) {
237 self.currentRequest = null;
237 self.currentRequest = null;
238
238
239 // review rules
239 // review rules
240 self.loadReviewRules(data);
240 self.loadReviewRules(data);
241
241
242 for (var i = 0; i < data.reviewers.length; i++) {
242 for (var i = 0; i < data.reviewers.length; i++) {
243 var reviewer = data.reviewers[i];
243 var reviewer = data.reviewers[i];
244 self.addReviewMember(
244 self.addReviewMember(
245 reviewer, reviewer.reasons, reviewer.mandatory);
245 reviewer, reviewer.reasons, reviewer.mandatory);
246 }
246 }
247 $('.calculate-reviewers').hide();
247 $('.calculate-reviewers').hide();
248 prButtonLock(false, null, 'reviewers');
248 prButtonLock(false, null, 'reviewers');
249 $('#user').show(); // show user autocomplete after load
249 $('#user').show(); // show user autocomplete after load
250 });
250 });
251 };
251 };
252
252
253 // check those, refactor
253 // check those, refactor
254 this.removeReviewMember = function(reviewer_id, mark_delete) {
254 this.removeReviewMember = function(reviewer_id, mark_delete) {
255 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
255 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
256
256
257 if(typeof(mark_delete) === undefined){
257 if(typeof(mark_delete) === undefined){
258 mark_delete = false;
258 mark_delete = false;
259 }
259 }
260
260
261 if(mark_delete === true){
261 if(mark_delete === true){
262 if (reviewer){
262 if (reviewer){
263 // now delete the input
263 // now delete the input
264 $('#reviewer_{0} input'.format(reviewer_id)).remove();
264 $('#reviewer_{0} input'.format(reviewer_id)).remove();
265 // mark as to-delete
265 // mark as to-delete
266 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
266 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
267 obj.addClass('to-delete');
267 obj.addClass('to-delete');
268 obj.css({"text-decoration":"line-through", "opacity": 0.5});
268 obj.css({"text-decoration":"line-through", "opacity": 0.5});
269 }
269 }
270 }
270 }
271 else{
271 else{
272 $('#reviewer_{0}'.format(reviewer_id)).remove();
272 $('#reviewer_{0}'.format(reviewer_id)).remove();
273 }
273 }
274 };
274 };
275 this.reviewMemberEntry = function() {
275 this.reviewMemberEntry = function() {
276
276
277 };
277 };
278 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
278 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
279 var members = self.$reviewMembers.get(0);
279 var members = self.$reviewMembers.get(0);
280 var id = reviewer_obj.user_id;
280 var id = reviewer_obj.user_id;
281 var username = reviewer_obj.username;
281 var username = reviewer_obj.username;
282
282
283 var reasons = reasons || [];
283 var reasons = reasons || [];
284 var mandatory = mandatory || false;
284 var mandatory = mandatory || false;
285
285
286 // register IDS to check if we don't have this ID already in
286 // register IDS to check if we don't have this ID already in
287 var currentIds = [];
287 var currentIds = [];
288 var _els = self.$reviewMembers.find('li').toArray();
288 var _els = self.$reviewMembers.find('li').toArray();
289 for (el in _els){
289 for (el in _els){
290 currentIds.push(_els[el].id)
290 currentIds.push(_els[el].id)
291 }
291 }
292
292
293 var userAllowedReview = function(userId) {
293 var userAllowedReview = function(userId) {
294 var allowed = true;
294 var allowed = true;
295 $.each(self.forbidReviewUsers, function(index, member_data) {
295 $.each(self.forbidReviewUsers, function(index, member_data) {
296 if (parseInt(userId) === member_data['user_id']) {
296 if (parseInt(userId) === member_data['user_id']) {
297 allowed = false;
297 allowed = false;
298 return false // breaks the loop
298 return false // breaks the loop
299 }
299 }
300 });
300 });
301 return allowed
301 return allowed
302 };
302 };
303
303
304 var userAllowed = userAllowedReview(id);
304 var userAllowed = userAllowedReview(id);
305 if (!userAllowed){
305 if (!userAllowed){
306 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
306 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
307 } else {
307 } else {
308 // only add if it's not there
308 // only add if it's not there
309 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
309 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
310
310
311 if (alreadyReviewer) {
311 if (alreadyReviewer) {
312 alert(_gettext('User `{0}` already in reviewers').format(username));
312 alert(_gettext('User `{0}` already in reviewers').format(username));
313 } else {
313 } else {
314 members.innerHTML += renderTemplate('reviewMemberEntry', {
314 members.innerHTML += renderTemplate('reviewMemberEntry', {
315 'member': reviewer_obj,
315 'member': reviewer_obj,
316 'mandatory': mandatory,
316 'mandatory': mandatory,
317 'allowed_to_update': true,
317 'allowed_to_update': true,
318 'review_status': 'not_reviewed',
318 'review_status': 'not_reviewed',
319 'review_status_label': _gettext('Not Reviewed'),
319 'review_status_label': _gettext('Not Reviewed'),
320 'reasons': reasons,
320 'reasons': reasons,
321 'create': true
321 'create': true
322 });
322 });
323 tooltipActivate();
323 tooltipActivate();
324 }
324 }
325 }
325 }
326
326
327 };
327 };
328
328
329 this.updateReviewers = function(repo_name, pull_request_id){
329 this.updateReviewers = function(repo_name, pull_request_id){
330 var postData = $('#reviewers input').serialize();
330 var postData = $('#reviewers input').serialize();
331 _updatePullRequest(repo_name, pull_request_id, postData);
331 _updatePullRequest(repo_name, pull_request_id, postData);
332 };
332 };
333
333
334 };
334 };
335
335
336
336
337 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
337 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
338 var url = pyroutes.url(
338 var url = pyroutes.url(
339 'pullrequest_update',
339 'pullrequest_update',
340 {"repo_name": repo_name, "pull_request_id": pull_request_id});
340 {"repo_name": repo_name, "pull_request_id": pull_request_id});
341 if (typeof postData === 'string' ) {
341 if (typeof postData === 'string' ) {
342 postData += '&csrf_token=' + CSRF_TOKEN;
342 postData += '&csrf_token=' + CSRF_TOKEN;
343 } else {
343 } else {
344 postData.csrf_token = CSRF_TOKEN;
344 postData.csrf_token = CSRF_TOKEN;
345 }
345 }
346
346 var success = function(o) {
347 var success = function(o) {
347 window.location.reload();
348 var redirectUrl = o['redirect_url'];
349 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
350 window.location = redirectUrl;
351 } else {
352 window.location.reload();
353 }
348 };
354 };
355
349 ajaxPOST(url, postData, success);
356 ajaxPOST(url, postData, success);
350 };
357 };
351
358
352 /**
359 /**
353 * PULL REQUEST update commits
360 * PULL REQUEST update commits
354 */
361 */
355 var updateCommits = function(repo_name, pull_request_id) {
362 var updateCommits = function(repo_name, pull_request_id, force) {
356 var postData = {
363 var postData = {
357 'update_commits': true};
364 'update_commits': true
365 };
366 if (force !== undefined && force === true) {
367 postData['force_refresh'] = true
368 }
358 _updatePullRequest(repo_name, pull_request_id, postData);
369 _updatePullRequest(repo_name, pull_request_id, postData);
359 };
370 };
360
371
361
372
362 /**
373 /**
363 * PULL REQUEST edit info
374 * PULL REQUEST edit info
364 */
375 */
365 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
376 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
366 var url = pyroutes.url(
377 var url = pyroutes.url(
367 'pullrequest_update',
378 'pullrequest_update',
368 {"repo_name": repo_name, "pull_request_id": pull_request_id});
379 {"repo_name": repo_name, "pull_request_id": pull_request_id});
369
380
370 var postData = {
381 var postData = {
371 'title': title,
382 'title': title,
372 'description': description,
383 'description': description,
373 'description_renderer': renderer,
384 'description_renderer': renderer,
374 'edit_pull_request': true,
385 'edit_pull_request': true,
375 'csrf_token': CSRF_TOKEN
386 'csrf_token': CSRF_TOKEN
376 };
387 };
377 var success = function(o) {
388 var success = function(o) {
378 window.location.reload();
389 window.location.reload();
379 };
390 };
380 ajaxPOST(url, postData, success);
391 ajaxPOST(url, postData, success);
381 };
392 };
382
393
383
394
384 /**
395 /**
385 * Reviewer autocomplete
396 * Reviewer autocomplete
386 */
397 */
387 var ReviewerAutoComplete = function(inputId) {
398 var ReviewerAutoComplete = function(inputId) {
388 $(inputId).autocomplete({
399 $(inputId).autocomplete({
389 serviceUrl: pyroutes.url('user_autocomplete_data'),
400 serviceUrl: pyroutes.url('user_autocomplete_data'),
390 minChars:2,
401 minChars:2,
391 maxHeight:400,
402 maxHeight:400,
392 deferRequestBy: 300, //miliseconds
403 deferRequestBy: 300, //miliseconds
393 showNoSuggestionNotice: true,
404 showNoSuggestionNotice: true,
394 tabDisabled: true,
405 tabDisabled: true,
395 autoSelectFirst: true,
406 autoSelectFirst: true,
396 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
407 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
397 formatResult: autocompleteFormatResult,
408 formatResult: autocompleteFormatResult,
398 lookupFilter: autocompleteFilterResult,
409 lookupFilter: autocompleteFilterResult,
399 onSelect: function(element, data) {
410 onSelect: function(element, data) {
400 var mandatory = false;
411 var mandatory = false;
401 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
412 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
402
413
403 // add whole user groups
414 // add whole user groups
404 if (data.value_type == 'user_group') {
415 if (data.value_type == 'user_group') {
405 reasons.push(_gettext('member of "{0}"').format(data.value_display));
416 reasons.push(_gettext('member of "{0}"').format(data.value_display));
406
417
407 $.each(data.members, function(index, member_data) {
418 $.each(data.members, function(index, member_data) {
408 var reviewer = member_data;
419 var reviewer = member_data;
409 reviewer['user_id'] = member_data['id'];
420 reviewer['user_id'] = member_data['id'];
410 reviewer['gravatar_link'] = member_data['icon_link'];
421 reviewer['gravatar_link'] = member_data['icon_link'];
411 reviewer['user_link'] = member_data['profile_link'];
422 reviewer['user_link'] = member_data['profile_link'];
412 reviewer['rules'] = [];
423 reviewer['rules'] = [];
413 reviewersController.addReviewMember(reviewer, reasons, mandatory);
424 reviewersController.addReviewMember(reviewer, reasons, mandatory);
414 })
425 })
415 }
426 }
416 // add single user
427 // add single user
417 else {
428 else {
418 var reviewer = data;
429 var reviewer = data;
419 reviewer['user_id'] = data['id'];
430 reviewer['user_id'] = data['id'];
420 reviewer['gravatar_link'] = data['icon_link'];
431 reviewer['gravatar_link'] = data['icon_link'];
421 reviewer['user_link'] = data['profile_link'];
432 reviewer['user_link'] = data['profile_link'];
422 reviewer['rules'] = [];
433 reviewer['rules'] = [];
423 reviewersController.addReviewMember(reviewer, reasons, mandatory);
434 reviewersController.addReviewMember(reviewer, reasons, mandatory);
424 }
435 }
425
436
426 $(inputId).val('');
437 $(inputId).val('');
427 }
438 }
428 });
439 });
429 };
440 };
430
441
431
442
432 VersionController = function () {
443 VersionController = function () {
433 var self = this;
444 var self = this;
434 this.$verSource = $('input[name=ver_source]');
445 this.$verSource = $('input[name=ver_source]');
435 this.$verTarget = $('input[name=ver_target]');
446 this.$verTarget = $('input[name=ver_target]');
436 this.$showVersionDiff = $('#show-version-diff');
447 this.$showVersionDiff = $('#show-version-diff');
437
448
438 this.adjustRadioSelectors = function (curNode) {
449 this.adjustRadioSelectors = function (curNode) {
439 var getVal = function (item) {
450 var getVal = function (item) {
440 if (item == 'latest') {
451 if (item == 'latest') {
441 return Number.MAX_SAFE_INTEGER
452 return Number.MAX_SAFE_INTEGER
442 }
453 }
443 else {
454 else {
444 return parseInt(item)
455 return parseInt(item)
445 }
456 }
446 };
457 };
447
458
448 var curVal = getVal($(curNode).val());
459 var curVal = getVal($(curNode).val());
449 var cleared = false;
460 var cleared = false;
450
461
451 $.each(self.$verSource, function (index, value) {
462 $.each(self.$verSource, function (index, value) {
452 var elVal = getVal($(value).val());
463 var elVal = getVal($(value).val());
453
464
454 if (elVal > curVal) {
465 if (elVal > curVal) {
455 if ($(value).is(':checked')) {
466 if ($(value).is(':checked')) {
456 cleared = true;
467 cleared = true;
457 }
468 }
458 $(value).attr('disabled', 'disabled');
469 $(value).attr('disabled', 'disabled');
459 $(value).removeAttr('checked');
470 $(value).removeAttr('checked');
460 $(value).css({'opacity': 0.1});
471 $(value).css({'opacity': 0.1});
461 }
472 }
462 else {
473 else {
463 $(value).css({'opacity': 1});
474 $(value).css({'opacity': 1});
464 $(value).removeAttr('disabled');
475 $(value).removeAttr('disabled');
465 }
476 }
466 });
477 });
467
478
468 if (cleared) {
479 if (cleared) {
469 // if we unchecked an active, set the next one to same loc.
480 // if we unchecked an active, set the next one to same loc.
470 $(this.$verSource).filter('[value={0}]'.format(
481 $(this.$verSource).filter('[value={0}]'.format(
471 curVal)).attr('checked', 'checked');
482 curVal)).attr('checked', 'checked');
472 }
483 }
473
484
474 self.setLockAction(false,
485 self.setLockAction(false,
475 $(curNode).data('verPos'),
486 $(curNode).data('verPos'),
476 $(this.$verSource).filter(':checked').data('verPos')
487 $(this.$verSource).filter(':checked').data('verPos')
477 );
488 );
478 };
489 };
479
490
480
491
481 this.attachVersionListener = function () {
492 this.attachVersionListener = function () {
482 self.$verTarget.change(function (e) {
493 self.$verTarget.change(function (e) {
483 self.adjustRadioSelectors(this)
494 self.adjustRadioSelectors(this)
484 });
495 });
485 self.$verSource.change(function (e) {
496 self.$verSource.change(function (e) {
486 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
497 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
487 });
498 });
488 };
499 };
489
500
490 this.init = function () {
501 this.init = function () {
491
502
492 var curNode = self.$verTarget.filter(':checked');
503 var curNode = self.$verTarget.filter(':checked');
493 self.adjustRadioSelectors(curNode);
504 self.adjustRadioSelectors(curNode);
494 self.setLockAction(true);
505 self.setLockAction(true);
495 self.attachVersionListener();
506 self.attachVersionListener();
496
507
497 };
508 };
498
509
499 this.setLockAction = function (state, selectedVersion, otherVersion) {
510 this.setLockAction = function (state, selectedVersion, otherVersion) {
500 var $showVersionDiff = this.$showVersionDiff;
511 var $showVersionDiff = this.$showVersionDiff;
501
512
502 if (state) {
513 if (state) {
503 $showVersionDiff.attr('disabled', 'disabled');
514 $showVersionDiff.attr('disabled', 'disabled');
504 $showVersionDiff.addClass('disabled');
515 $showVersionDiff.addClass('disabled');
505 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
516 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
506 }
517 }
507 else {
518 else {
508 $showVersionDiff.removeAttr('disabled');
519 $showVersionDiff.removeAttr('disabled');
509 $showVersionDiff.removeClass('disabled');
520 $showVersionDiff.removeClass('disabled');
510
521
511 if (selectedVersion == otherVersion) {
522 if (selectedVersion == otherVersion) {
512 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
523 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
513 } else {
524 } else {
514 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
525 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
515 }
526 }
516 }
527 }
517
528
518 };
529 };
519
530
520 this.showVersionDiff = function () {
531 this.showVersionDiff = function () {
521 var target = self.$verTarget.filter(':checked');
532 var target = self.$verTarget.filter(':checked');
522 var source = self.$verSource.filter(':checked');
533 var source = self.$verSource.filter(':checked');
523
534
524 if (target.val() && source.val()) {
535 if (target.val() && source.val()) {
525 var params = {
536 var params = {
526 'pull_request_id': templateContext.pull_request_data.pull_request_id,
537 'pull_request_id': templateContext.pull_request_data.pull_request_id,
527 'repo_name': templateContext.repo_name,
538 'repo_name': templateContext.repo_name,
528 'version': target.val(),
539 'version': target.val(),
529 'from_version': source.val()
540 'from_version': source.val()
530 };
541 };
531 window.location = pyroutes.url('pullrequest_show', params)
542 window.location = pyroutes.url('pullrequest_show', params)
532 }
543 }
533
544
534 return false;
545 return false;
535 };
546 };
536
547
537 this.toggleVersionView = function (elem) {
548 this.toggleVersionView = function (elem) {
538
549
539 if (this.$showVersionDiff.is(':visible')) {
550 if (this.$showVersionDiff.is(':visible')) {
540 $('.version-pr').hide();
551 $('.version-pr').hide();
541 this.$showVersionDiff.hide();
552 this.$showVersionDiff.hide();
542 $(elem).html($(elem).data('toggleOn'))
553 $(elem).html($(elem).data('toggleOn'))
543 } else {
554 } else {
544 $('.version-pr').show();
555 $('.version-pr').show();
545 this.$showVersionDiff.show();
556 this.$showVersionDiff.show();
546 $(elem).html($(elem).data('toggleOff'))
557 $(elem).html($(elem).data('toggleOff'))
547 }
558 }
548
559
549 return false
560 return false
550 }
561 }
551
562
563 };
564
565
566 UpdatePrController = function () {
567 var self = this;
568 this.$updateCommits = $('#update_commits');
569 this.$updateCommitsSwitcher = $('#update_commits_switcher');
570
571 this.lockUpdateButton = function (label) {
572 self.$updateCommits.attr('disabled', 'disabled');
573 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
574
575 self.$updateCommits.addClass('disabled');
576 self.$updateCommitsSwitcher.addClass('disabled');
577
578 self.$updateCommits.removeClass('btn-primary');
579 self.$updateCommitsSwitcher.removeClass('btn-primary');
580
581 self.$updateCommits.text(_gettext(label));
582 };
583
584 this.isUpdateLocked = function () {
585 return self.$updateCommits.attr('disabled') !== undefined;
586 };
587
588 this.updateCommits = function (curNode) {
589 if (self.isUpdateLocked()) {
590 return
591 }
592 self.lockUpdateButton(_gettext('Updating...'));
593 updateCommits(
594 templateContext.repo_name,
595 templateContext.pull_request_data.pull_request_id);
596 };
597
598 this.forceUpdateCommits = function () {
599 if (self.isUpdateLocked()) {
600 return
601 }
602 self.lockUpdateButton(_gettext('Force updating...'));
603 var force = true;
604 updateCommits(
605 templateContext.repo_name,
606 templateContext.pull_request_data.pull_request_id, force);
607 };
552 }; No newline at end of file
608 };
@@ -1,84 +1,82 b''
1
1
2 <div class="pull-request-wrap">
2 <div class="pull-request-wrap">
3
3
4 % if c.pr_merge_possible:
4 % if c.pr_merge_possible:
5 <h2 class="merge-status">
5 <h2 class="merge-status">
6 <span class="merge-icon success"><i class="icon-ok"></i></span>
6 <span class="merge-icon success"><i class="icon-ok"></i></span>
7 ${_('This pull request can be merged automatically.')}
7 ${_('This pull request can be merged automatically.')}
8 </h2>
8 </h2>
9 % else:
9 % else:
10 <h2 class="merge-status">
10 <h2 class="merge-status">
11 <span class="merge-icon warning"><i class="icon-false"></i></span>
11 <span class="merge-icon warning"><i class="icon-false"></i></span>
12 ${_('Merge is not currently possible because of below failed checks.')}
12 ${_('Merge is not currently possible because of below failed checks.')}
13 </h2>
13 </h2>
14 % endif
14 % endif
15
15
16 % if c.pr_merge_errors.items():
16 % if c.pr_merge_errors.items():
17 <ul>
17 <ul>
18 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
18 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
19 <% pr_check_type = pr_check_details['error_type'] %>
19 <% pr_check_type = pr_check_details['error_type'] %>
20 <li>
20 <li>
21 <div class="merge-message ${pr_check_type}" data-role="merge-message">
21 <div class="merge-message ${pr_check_type}" data-role="merge-message">
22 <span style="white-space: pre-line">- ${pr_check_details['message']}</span>
22 <span style="white-space: pre-line">- ${pr_check_details['message']}</span>
23 % if pr_check_key == 'todo':
23 % if pr_check_key == 'todo':
24 % for co in pr_check_details['details']:
24 % for co in pr_check_details['details']:
25 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
25 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
26 % endfor
26 % endfor
27 % endif
27 % endif
28 </div>
28 </div>
29 </li>
29 </li>
30 % endfor
30 % endfor
31 </ul>
31 </ul>
32 % endif
32 % endif
33
33
34 <div class="pull-request-merge-actions">
34 <div class="pull-request-merge-actions">
35 % if c.allowed_to_merge:
35 % if c.allowed_to_merge:
36 ## Merge info, show only if all errors are taken care of
36 ## Merge info, show only if all errors are taken care of
37 % if not c.pr_merge_errors and c.pr_merge_info:
37 % if not c.pr_merge_errors and c.pr_merge_info:
38 <div class="pull-request-merge-info">
38 <div class="pull-request-merge-info">
39 <ul>
39 <ul>
40 % for pr_merge_key, pr_merge_details in c.pr_merge_info.items():
40 % for pr_merge_key, pr_merge_details in c.pr_merge_info.items():
41 <li>
41 <li>
42 - ${pr_merge_details['message']}
42 - ${pr_merge_details['message']}
43 </li>
43 </li>
44 % endfor
44 % endfor
45 </ul>
45 </ul>
46 </div>
46 </div>
47 % endif
47 % endif
48
48
49 <div>
49 <div>
50 ${h.secure_form(h.route_path('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form', request=request)}
50 ${h.secure_form(h.route_path('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form', request=request)}
51 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
51 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
52
52
53 % if c.allowed_to_close:
53 % if c.allowed_to_close:
54 ## close PR action, injected later next to COMMENT button
54 ## close PR action, injected later next to COMMENT button
55 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
55 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
56 <a id="close-pull-request-action" class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
56 <a id="close-pull-request-action" class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
57 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
57 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
58 </a>
58 </a>
59 % else:
59 % else:
60 <a id="close-pull-request-action" class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
60 <a id="close-pull-request-action" class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
61 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
61 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
62 </a>
62 </a>
63 % endif
63 % endif
64 % endif
64 % endif
65
65
66 <input type="submit" id="merge_pull_request" value="${_('Merge and close Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
66 <input type="submit" id="merge_pull_request" value="${_('Merge and close Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
67 ${h.end_form()}
67 ${h.end_form()}
68
68
69 <div class="pull-request-merge-refresh">
69 <div class="pull-request-merge-refresh">
70 <a href="#refreshChecks" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
70 <a href="#refreshChecks" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
71 /
72 <a class="tooltip" title="Force refresh of the merge workspace in case current status seems wrong." href="${h.route_path('pullrequest_show', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id,_query={"force_refresh":1})}">forced recheck</a>
73 </div>
71 </div>
74
72
75 </div>
73 </div>
76 % elif c.rhodecode_user.username != h.DEFAULT_USER:
74 % elif c.rhodecode_user.username != h.DEFAULT_USER:
77 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
75 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
78 <input type="submit" value="${_('Merge and close Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
76 <input type="submit" value="${_('Merge and close Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
79 % else:
77 % else:
80 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
78 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
81 % endif
79 % endif
82 </div>
80 </div>
83
81
84 </div>
82 </div>
@@ -1,794 +1,809 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
7 %if c.rhodecode_name:
7 %if c.rhodecode_name:
8 &middot; ${h.branding(c.rhodecode_name)}
8 &middot; ${h.branding(c.rhodecode_name)}
9 %endif
9 %endif
10 </%def>
10 </%def>
11
11
12 <%def name="breadcrumbs_links()">
12 <%def name="breadcrumbs_links()">
13 <span id="pr-title">
13 <span id="pr-title">
14 ${c.pull_request.title}
14 ${c.pull_request.title}
15 %if c.pull_request.is_closed():
15 %if c.pull_request.is_closed():
16 (${_('Closed')})
16 (${_('Closed')})
17 %endif
17 %endif
18 </span>
18 </span>
19 <div id="pr-title-edit" class="input" style="display: none;">
19 <div id="pr-title-edit" class="input" style="display: none;">
20 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
21 </div>
21 </div>
22 </%def>
22 </%def>
23
23
24 <%def name="menu_bar_nav()">
24 <%def name="menu_bar_nav()">
25 ${self.menu_items(active='repositories')}
25 ${self.menu_items(active='repositories')}
26 </%def>
26 </%def>
27
27
28 <%def name="menu_bar_subnav()">
28 <%def name="menu_bar_subnav()">
29 ${self.repo_menu(active='showpullrequest')}
29 ${self.repo_menu(active='showpullrequest')}
30 </%def>
30 </%def>
31
31
32 <%def name="main()">
32 <%def name="main()">
33
33
34 <script type="text/javascript">
34 <script type="text/javascript">
35 // TODO: marcink switch this to pyroutes
35 // TODO: marcink switch this to pyroutes
36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 </script>
38 </script>
39 <div class="box">
39 <div class="box">
40
40
41 ${self.breadcrumbs()}
41 ${self.breadcrumbs()}
42
42
43 <div class="box pr-summary">
43 <div class="box pr-summary">
44
44
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 <div class="pr-details-title">
47 <div class="pr-details-title">
48 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
49 %if c.allowed_to_update:
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 % if c.allowed_to_delete:
51 % if c.allowed_to_delete:
52 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
52 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
54 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 ${h.end_form()}
55 ${h.end_form()}
56 % else:
56 % else:
57 ${_('Delete')}
57 ${_('Delete')}
58 % endif
58 % endif
59 </div>
59 </div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 %endif
62 %endif
63 </div>
63 </div>
64
64
65 <div id="summary" class="fields pr-details-content">
65 <div id="summary" class="fields pr-details-content">
66 <div class="field">
66 <div class="field">
67 <div class="label-summary">
67 <div class="label-summary">
68 <label>${_('Source')}:</label>
68 <label>${_('Source')}:</label>
69 </div>
69 </div>
70 <div class="input">
70 <div class="input">
71 <div class="pr-origininfo">
71 <div class="pr-origininfo">
72 ## branch link is only valid if it is a branch
72 ## branch link is only valid if it is a branch
73 <span class="tag">
73 <span class="tag">
74 %if c.pull_request.source_ref_parts.type == 'branch':
74 %if c.pull_request.source_ref_parts.type == 'branch':
75 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
75 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 %else:
76 %else:
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 %endif
78 %endif
79 </span>
79 </span>
80 <span class="clone-url">
80 <span class="clone-url">
81 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
81 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 </span>
82 </span>
83 <br/>
83 <br/>
84 % if c.ancestor_commit:
84 % if c.ancestor_commit:
85 ${_('Common ancestor')}:
85 ${_('Common ancestor')}:
86 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
86 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
87 % endif
87 % endif
88 </div>
88 </div>
89 %if h.is_hg(c.pull_request.source_repo):
89 %if h.is_hg(c.pull_request.source_repo):
90 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
90 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
91 %elif h.is_git(c.pull_request.source_repo):
91 %elif h.is_git(c.pull_request.source_repo):
92 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
92 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
93 %endif
93 %endif
94
94
95 <div class="">
95 <div class="">
96 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
96 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
97 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
97 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
98 </div>
98 </div>
99
99
100 </div>
100 </div>
101 </div>
101 </div>
102 <div class="field">
102 <div class="field">
103 <div class="label-summary">
103 <div class="label-summary">
104 <label>${_('Target')}:</label>
104 <label>${_('Target')}:</label>
105 </div>
105 </div>
106 <div class="input">
106 <div class="input">
107 <div class="pr-targetinfo">
107 <div class="pr-targetinfo">
108 ## branch link is only valid if it is a branch
108 ## branch link is only valid if it is a branch
109 <span class="tag">
109 <span class="tag">
110 %if c.pull_request.target_ref_parts.type == 'branch':
110 %if c.pull_request.target_ref_parts.type == 'branch':
111 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
112 %else:
112 %else:
113 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
114 %endif
114 %endif
115 </span>
115 </span>
116 <span class="clone-url">
116 <span class="clone-url">
117 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
118 </span>
118 </span>
119 </div>
119 </div>
120 </div>
120 </div>
121 </div>
121 </div>
122
122
123 ## Link to the shadow repository.
123 ## Link to the shadow repository.
124 <div class="field">
124 <div class="field">
125 <div class="label-summary">
125 <div class="label-summary">
126 <label>${_('Merge')}:</label>
126 <label>${_('Merge')}:</label>
127 </div>
127 </div>
128 <div class="input">
128 <div class="input">
129 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
130 %if h.is_hg(c.pull_request.target_repo):
130 %if h.is_hg(c.pull_request.target_repo):
131 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
131 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
132 %elif h.is_git(c.pull_request.target_repo):
132 %elif h.is_git(c.pull_request.target_repo):
133 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
133 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
134 %endif
134 %endif
135 <div class="">
135 <div class="">
136 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
136 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
137 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
137 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
138 </div>
138 </div>
139 % else:
139 % else:
140 <div class="">
140 <div class="">
141 ${_('Shadow repository data not available')}.
141 ${_('Shadow repository data not available')}.
142 </div>
142 </div>
143 % endif
143 % endif
144 </div>
144 </div>
145 </div>
145 </div>
146
146
147 <div class="field">
147 <div class="field">
148 <div class="label-summary">
148 <div class="label-summary">
149 <label>${_('Review')}:</label>
149 <label>${_('Review')}:</label>
150 </div>
150 </div>
151 <div class="input">
151 <div class="input">
152 %if c.pull_request_review_status:
152 %if c.pull_request_review_status:
153 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
153 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
154 <span class="changeset-status-lbl tooltip">
154 <span class="changeset-status-lbl tooltip">
155 %if c.pull_request.is_closed():
155 %if c.pull_request.is_closed():
156 ${_('Closed')},
156 ${_('Closed')},
157 %endif
157 %endif
158 ${h.commit_status_lbl(c.pull_request_review_status)}
158 ${h.commit_status_lbl(c.pull_request_review_status)}
159 </span>
159 </span>
160 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
160 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
161 %endif
161 %endif
162 </div>
162 </div>
163 </div>
163 </div>
164 <div class="field">
164 <div class="field">
165 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
165 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
166 <label>${_('Description')}:</label>
166 <label>${_('Description')}:</label>
167 </div>
167 </div>
168 <div id="pr-desc" class="input">
168 <div id="pr-desc" class="input">
169 <div class="pr-description">${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}</div>
169 <div class="pr-description">${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}</div>
170 </div>
170 </div>
171 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
171 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
172 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
172 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
173 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
173 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
174 </div>
174 </div>
175 </div>
175 </div>
176
176
177 <div class="field">
177 <div class="field">
178 <div class="label-summary">
178 <div class="label-summary">
179 <label>${_('Versions')}:</label>
179 <label>${_('Versions')}:</label>
180 </div>
180 </div>
181
181
182 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
182 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
183 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
183 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
184
184
185 <div class="pr-versions">
185 <div class="pr-versions">
186 % if c.show_version_changes:
186 % if c.show_version_changes:
187 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
187 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
188 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
188 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
189 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
189 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
190 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
190 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
191 data-toggle-off="${_('Hide all versions of this pull request')}">
191 data-toggle-off="${_('Hide all versions of this pull request')}">
192 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
192 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
193 </a>
193 </a>
194 <table>
194 <table>
195 ## SHOW ALL VERSIONS OF PR
195 ## SHOW ALL VERSIONS OF PR
196 <% ver_pr = None %>
196 <% ver_pr = None %>
197
197
198 % for data in reversed(list(enumerate(c.versions, 1))):
198 % for data in reversed(list(enumerate(c.versions, 1))):
199 <% ver_pos = data[0] %>
199 <% ver_pos = data[0] %>
200 <% ver = data[1] %>
200 <% ver = data[1] %>
201 <% ver_pr = ver.pull_request_version_id %>
201 <% ver_pr = ver.pull_request_version_id %>
202 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
202 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
203
203
204 <tr class="version-pr" style="display: ${display_row}">
204 <tr class="version-pr" style="display: ${display_row}">
205 <td>
205 <td>
206 <code>
206 <code>
207 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
207 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
208 </code>
208 </code>
209 </td>
209 </td>
210 <td>
210 <td>
211 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
211 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
212 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
212 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
213 </td>
213 </td>
214 <td>
214 <td>
215 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
215 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
216 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
216 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
217 </div>
217 </div>
218 </td>
218 </td>
219 <td>
219 <td>
220 % if c.at_version_num != ver_pr:
220 % if c.at_version_num != ver_pr:
221 <i class="icon-comment"></i>
221 <i class="icon-comment"></i>
222 <code class="tooltip" title="${_('Comment from pull request version v{0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
222 <code class="tooltip" title="${_('Comment from pull request version v{0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
223 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
223 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
224 </code>
224 </code>
225 % endif
225 % endif
226 </td>
226 </td>
227 <td>
227 <td>
228 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
228 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
229 </td>
229 </td>
230 <td>
230 <td>
231 ${h.age_component(ver.updated_on, time_is_local=True)}
231 ${h.age_component(ver.updated_on, time_is_local=True)}
232 </td>
232 </td>
233 </tr>
233 </tr>
234 % endfor
234 % endfor
235
235
236 <tr>
236 <tr>
237 <td colspan="6">
237 <td colspan="6">
238 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
238 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
239 data-label-text-locked="${_('select versions to show changes')}"
239 data-label-text-locked="${_('select versions to show changes')}"
240 data-label-text-diff="${_('show changes between versions')}"
240 data-label-text-diff="${_('show changes between versions')}"
241 data-label-text-show="${_('show pull request for this version')}"
241 data-label-text-show="${_('show pull request for this version')}"
242 >
242 >
243 ${_('select versions to show changes')}
243 ${_('select versions to show changes')}
244 </button>
244 </button>
245 </td>
245 </td>
246 </tr>
246 </tr>
247 </table>
247 </table>
248 % else:
248 % else:
249 <div class="input">
249 <div class="input">
250 ${_('Pull request versions not available')}.
250 ${_('Pull request versions not available')}.
251 </div>
251 </div>
252 % endif
252 % endif
253 </div>
253 </div>
254 </div>
254 </div>
255
255
256 <div id="pr-save" class="field" style="display: none;">
256 <div id="pr-save" class="field" style="display: none;">
257 <div class="label-summary"></div>
257 <div class="label-summary"></div>
258 <div class="input">
258 <div class="input">
259 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
259 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
260 </div>
260 </div>
261 </div>
261 </div>
262 </div>
262 </div>
263 </div>
263 </div>
264 <div>
264 <div>
265 ## AUTHOR
265 ## AUTHOR
266 <div class="reviewers-title block-right">
266 <div class="reviewers-title block-right">
267 <div class="pr-details-title">
267 <div class="pr-details-title">
268 ${_('Author of this pull request')}
268 ${_('Author of this pull request')}
269 </div>
269 </div>
270 </div>
270 </div>
271 <div class="block-right pr-details-content reviewers">
271 <div class="block-right pr-details-content reviewers">
272 <ul class="group_members">
272 <ul class="group_members">
273 <li>
273 <li>
274 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
274 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
275 </li>
275 </li>
276 </ul>
276 </ul>
277 </div>
277 </div>
278
278
279 ## REVIEW RULES
279 ## REVIEW RULES
280 <div id="review_rules" style="display: none" class="reviewers-title block-right">
280 <div id="review_rules" style="display: none" class="reviewers-title block-right">
281 <div class="pr-details-title">
281 <div class="pr-details-title">
282 ${_('Reviewer rules')}
282 ${_('Reviewer rules')}
283 %if c.allowed_to_update:
283 %if c.allowed_to_update:
284 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
284 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
285 %endif
285 %endif
286 </div>
286 </div>
287 <div class="pr-reviewer-rules">
287 <div class="pr-reviewer-rules">
288 ## review rules will be appended here, by default reviewers logic
288 ## review rules will be appended here, by default reviewers logic
289 </div>
289 </div>
290 <input id="review_data" type="hidden" name="review_data" value="">
290 <input id="review_data" type="hidden" name="review_data" value="">
291 </div>
291 </div>
292
292
293 ## REVIEWERS
293 ## REVIEWERS
294 <div class="reviewers-title block-right">
294 <div class="reviewers-title block-right">
295 <div class="pr-details-title">
295 <div class="pr-details-title">
296 ${_('Pull request reviewers')}
296 ${_('Pull request reviewers')}
297 %if c.allowed_to_update:
297 %if c.allowed_to_update:
298 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
298 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
299 %endif
299 %endif
300 </div>
300 </div>
301 </div>
301 </div>
302 <div id="reviewers" class="block-right pr-details-content reviewers">
302 <div id="reviewers" class="block-right pr-details-content reviewers">
303
303
304 ## members redering block
304 ## members redering block
305 <input type="hidden" name="__start__" value="review_members:sequence">
305 <input type="hidden" name="__start__" value="review_members:sequence">
306 <ul id="review_members" class="group_members">
306 <ul id="review_members" class="group_members">
307
307
308 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
308 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
309 <script>
309 <script>
310 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
310 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
311 var status = "${(status[0][1].status if status else 'not_reviewed')}";
311 var status = "${(status[0][1].status if status else 'not_reviewed')}";
312 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
312 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
313 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
313 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
314
314
315 var entry = renderTemplate('reviewMemberEntry', {
315 var entry = renderTemplate('reviewMemberEntry', {
316 'member': member,
316 'member': member,
317 'mandatory': member.mandatory,
317 'mandatory': member.mandatory,
318 'reasons': member.reasons,
318 'reasons': member.reasons,
319 'allowed_to_update': allowed_to_update,
319 'allowed_to_update': allowed_to_update,
320 'review_status': status,
320 'review_status': status,
321 'review_status_label': status_lbl,
321 'review_status_label': status_lbl,
322 'user_group': member.user_group,
322 'user_group': member.user_group,
323 'create': false
323 'create': false
324 });
324 });
325 $('#review_members').append(entry)
325 $('#review_members').append(entry)
326 </script>
326 </script>
327
327
328 % endfor
328 % endfor
329
329
330 </ul>
330 </ul>
331
331
332 <input type="hidden" name="__end__" value="review_members:sequence">
332 <input type="hidden" name="__end__" value="review_members:sequence">
333 ## end members redering block
333 ## end members redering block
334
334
335 %if not c.pull_request.is_closed():
335 %if not c.pull_request.is_closed():
336 <div id="add_reviewer" class="ac" style="display: none;">
336 <div id="add_reviewer" class="ac" style="display: none;">
337 %if c.allowed_to_update:
337 %if c.allowed_to_update:
338 % if not c.forbid_adding_reviewers:
338 % if not c.forbid_adding_reviewers:
339 <div id="add_reviewer_input" class="reviewer_ac">
339 <div id="add_reviewer_input" class="reviewer_ac">
340 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
340 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
341 <div id="reviewers_container"></div>
341 <div id="reviewers_container"></div>
342 </div>
342 </div>
343 % endif
343 % endif
344 <div class="pull-right">
344 <div class="pull-right">
345 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
345 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
346 </div>
346 </div>
347 %endif
347 %endif
348 </div>
348 </div>
349 %endif
349 %endif
350 </div>
350 </div>
351 </div>
351 </div>
352 </div>
352 </div>
353 <div class="box">
353 <div class="box">
354 ##DIFF
354 ##DIFF
355 <div class="table" >
355 <div class="table" >
356 <div id="changeset_compare_view_content">
356 <div id="changeset_compare_view_content">
357 ##CS
357 ##CS
358 % if c.missing_requirements:
358 % if c.missing_requirements:
359 <div class="box">
359 <div class="box">
360 <div class="alert alert-warning">
360 <div class="alert alert-warning">
361 <div>
361 <div>
362 <strong>${_('Missing requirements:')}</strong>
362 <strong>${_('Missing requirements:')}</strong>
363 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
363 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
364 </div>
364 </div>
365 </div>
365 </div>
366 </div>
366 </div>
367 % elif c.missing_commits:
367 % elif c.missing_commits:
368 <div class="box">
368 <div class="box">
369 <div class="alert alert-warning">
369 <div class="alert alert-warning">
370 <div>
370 <div>
371 <strong>${_('Missing commits')}:</strong>
371 <strong>${_('Missing commits')}:</strong>
372 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
372 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
373 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
373 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
374 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
374 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
375 </div>
375 </div>
376 </div>
376 </div>
377 </div>
377 </div>
378 % endif
378 % endif
379
379
380 <div class="compare_view_commits_title">
380 <div class="compare_view_commits_title">
381 % if not c.compare_mode:
381 % if not c.compare_mode:
382
382
383 % if c.at_version_pos:
383 % if c.at_version_pos:
384 <h4>
384 <h4>
385 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
385 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
386 </h4>
386 </h4>
387 % endif
387 % endif
388
388
389 <div class="pull-left">
389 <div class="pull-left">
390 <div class="btn-group">
390 <div class="btn-group">
391 <a
391 <a
392 class="btn"
392 class="btn"
393 href="#"
393 href="#"
394 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
394 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
395 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
395 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
396 </a>
396 </a>
397 <a
397 <a
398 class="btn"
398 class="btn"
399 href="#"
399 href="#"
400 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
400 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
401 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
401 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
402 </a>
402 </a>
403 </div>
403 </div>
404 </div>
404 </div>
405
405
406 <div class="pull-right">
406 <div class="pull-right">
407 % if c.allowed_to_update and not c.pull_request.is_closed():
407 % if c.allowed_to_update and not c.pull_request.is_closed():
408 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
408
409 <div class="btn-group btn-group-actions">
410 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
411 ${_('Update commits')}
412 </a>
413
414 <a id="update_commits_switcher" class="btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button">
415 <i class="icon-down"></i>
416 </a>
417
418 <div class="btn-action-switcher-container" id="update-commits-switcher">
419 <ul class="btn-action-switcher" role="menu">
420 <li>
421 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
422 ${_('Force update commits')}
423 </a>
424 <div class="action-help-block">
425 ${_('Update commits and force refresh this pull request.')}
426 </div>
427 </li>
428 </ul>
429 </div>
430 </div>
431
409 % else:
432 % else:
410 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
433 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
411 % endif
434 % endif
412
435
413 </div>
436 </div>
414 % endif
437 % endif
415 </div>
438 </div>
416
439
417 % if not c.missing_commits:
440 % if not c.missing_commits:
418 % if c.compare_mode:
441 % if c.compare_mode:
419 % if c.at_version:
442 % if c.at_version:
420 <h4>
443 <h4>
421 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
444 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
422 </h4>
445 </h4>
423
446
424 <div class="subtitle-compare">
447 <div class="subtitle-compare">
425 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
448 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
426 </div>
449 </div>
427
450
428 <div class="container">
451 <div class="container">
429 <table class="rctable compare_view_commits">
452 <table class="rctable compare_view_commits">
430 <tr>
453 <tr>
431 <th></th>
454 <th></th>
432 <th>${_('Time')}</th>
455 <th>${_('Time')}</th>
433 <th>${_('Author')}</th>
456 <th>${_('Author')}</th>
434 <th>${_('Commit')}</th>
457 <th>${_('Commit')}</th>
435 <th></th>
458 <th></th>
436 <th>${_('Description')}</th>
459 <th>${_('Description')}</th>
437 </tr>
460 </tr>
438
461
439 % for c_type, commit in c.commit_changes:
462 % for c_type, commit in c.commit_changes:
440 % if c_type in ['a', 'r']:
463 % if c_type in ['a', 'r']:
441 <%
464 <%
442 if c_type == 'a':
465 if c_type == 'a':
443 cc_title = _('Commit added in displayed changes')
466 cc_title = _('Commit added in displayed changes')
444 elif c_type == 'r':
467 elif c_type == 'r':
445 cc_title = _('Commit removed in displayed changes')
468 cc_title = _('Commit removed in displayed changes')
446 else:
469 else:
447 cc_title = ''
470 cc_title = ''
448 %>
471 %>
449 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
472 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
450 <td>
473 <td>
451 <div class="commit-change-indicator color-${c_type}-border">
474 <div class="commit-change-indicator color-${c_type}-border">
452 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
475 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
453 ${c_type.upper()}
476 ${c_type.upper()}
454 </div>
477 </div>
455 </div>
478 </div>
456 </td>
479 </td>
457 <td class="td-time">
480 <td class="td-time">
458 ${h.age_component(commit.date)}
481 ${h.age_component(commit.date)}
459 </td>
482 </td>
460 <td class="td-user">
483 <td class="td-user">
461 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
484 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
462 </td>
485 </td>
463 <td class="td-hash">
486 <td class="td-hash">
464 <code>
487 <code>
465 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
488 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
466 r${commit.idx}:${h.short_id(commit.raw_id)}
489 r${commit.idx}:${h.short_id(commit.raw_id)}
467 </a>
490 </a>
468 ${h.hidden('revisions', commit.raw_id)}
491 ${h.hidden('revisions', commit.raw_id)}
469 </code>
492 </code>
470 </td>
493 </td>
471 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
494 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
472 <i class="icon-expand-linked"></i>
495 <i class="icon-expand-linked"></i>
473 </td>
496 </td>
474 <td class="mid td-description">
497 <td class="mid td-description">
475 <div class="log-container truncate-wrap">
498 <div class="log-container truncate-wrap">
476 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
499 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
477 </div>
500 </div>
478 </td>
501 </td>
479 </tr>
502 </tr>
480 % endif
503 % endif
481 % endfor
504 % endfor
482 </table>
505 </table>
483 </div>
506 </div>
484
507
485 % endif
508 % endif
486
509
487 % else:
510 % else:
488 <%include file="/compare/compare_commits.mako" />
511 <%include file="/compare/compare_commits.mako" />
489 % endif
512 % endif
490
513
491 <div class="cs_files">
514 <div class="cs_files">
492 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
515 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
493 % if c.at_version:
516 % if c.at_version:
494 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
517 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
495 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
518 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
496 % else:
519 % else:
497 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
520 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
498 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
521 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
499 % endif
522 % endif
500
523
501 <%
524 <%
502 pr_menu_data = {
525 pr_menu_data = {
503 'outdated_comm_count_ver': outdated_comm_count_ver
526 'outdated_comm_count_ver': outdated_comm_count_ver
504 }
527 }
505 %>
528 %>
506
529
507 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
530 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
508
531
509 % if c.range_diff_on:
532 % if c.range_diff_on:
510 % for commit in c.commit_ranges:
533 % for commit in c.commit_ranges:
511 ${cbdiffs.render_diffset(
534 ${cbdiffs.render_diffset(
512 c.changes[commit.raw_id],
535 c.changes[commit.raw_id],
513 commit=commit, use_comments=True,
536 commit=commit, use_comments=True,
514 collapse_when_files_over=5,
537 collapse_when_files_over=5,
515 disable_new_comments=True,
538 disable_new_comments=True,
516 deleted_files_comments=c.deleted_files_comments,
539 deleted_files_comments=c.deleted_files_comments,
517 inline_comments=c.inline_comments,
540 inline_comments=c.inline_comments,
518 pull_request_menu=pr_menu_data)}
541 pull_request_menu=pr_menu_data)}
519 % endfor
542 % endfor
520 % else:
543 % else:
521 ${cbdiffs.render_diffset(
544 ${cbdiffs.render_diffset(
522 c.diffset, use_comments=True,
545 c.diffset, use_comments=True,
523 collapse_when_files_over=30,
546 collapse_when_files_over=30,
524 disable_new_comments=not c.allowed_to_comment,
547 disable_new_comments=not c.allowed_to_comment,
525 deleted_files_comments=c.deleted_files_comments,
548 deleted_files_comments=c.deleted_files_comments,
526 inline_comments=c.inline_comments,
549 inline_comments=c.inline_comments,
527 pull_request_menu=pr_menu_data)}
550 pull_request_menu=pr_menu_data)}
528 % endif
551 % endif
529
552
530 </div>
553 </div>
531 % else:
554 % else:
532 ## skipping commits we need to clear the view for missing commits
555 ## skipping commits we need to clear the view for missing commits
533 <div style="clear:both;"></div>
556 <div style="clear:both;"></div>
534 % endif
557 % endif
535
558
536 </div>
559 </div>
537 </div>
560 </div>
538
561
539 ## template for inline comment form
562 ## template for inline comment form
540 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
563 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
541
564
542 ## comments heading with count
565 ## comments heading with count
543 <div class="comments-heading">
566 <div class="comments-heading">
544 <i class="icon-comment"></i>
567 <i class="icon-comment"></i>
545 ${_('Comments')} ${len(c.comments)}
568 ${_('Comments')} ${len(c.comments)}
546 </div>
569 </div>
547
570
548 ## render general comments
571 ## render general comments
549 <div id="comment-tr-show">
572 <div id="comment-tr-show">
550 % if general_outdated_comm_count_ver:
573 % if general_outdated_comm_count_ver:
551 <div class="info-box">
574 <div class="info-box">
552 % if general_outdated_comm_count_ver == 1:
575 % if general_outdated_comm_count_ver == 1:
553 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
576 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
554 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
577 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
555 % else:
578 % else:
556 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
579 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
557 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
580 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
558 % endif
581 % endif
559 </div>
582 </div>
560 % endif
583 % endif
561 </div>
584 </div>
562
585
563 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
586 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
564
587
565 % if not c.pull_request.is_closed():
588 % if not c.pull_request.is_closed():
566 ## main comment form and it status
589 ## main comment form and it status
567 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
590 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
568 pull_request_id=c.pull_request.pull_request_id),
591 pull_request_id=c.pull_request.pull_request_id),
569 c.pull_request_review_status,
592 c.pull_request_review_status,
570 is_pull_request=True, change_status=c.allowed_to_change_status)}
593 is_pull_request=True, change_status=c.allowed_to_change_status)}
571
594
572 ## merge status, and merge action
595 ## merge status, and merge action
573 <div class="pull-request-merge">
596 <div class="pull-request-merge">
574 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
597 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
575 </div>
598 </div>
576
599
577 %endif
600 %endif
578
601
579 <script type="text/javascript">
602 <script type="text/javascript">
580
603
581 versionController = new VersionController();
604 versionController = new VersionController();
582 versionController.init();
605 versionController.init();
583
606
584 reviewersController = new ReviewersController();
607 reviewersController = new ReviewersController();
585 commitsController = new CommitsController();
608 commitsController = new CommitsController();
586
609
610 updateController = new UpdatePrController();
611
587 $(function(){
612 $(function(){
588
613
589 // custom code mirror
614 // custom code mirror
590 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
615 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
591
616
592 var PRDetails = {
617 var PRDetails = {
593 editButton: $('#open_edit_pullrequest'),
618 editButton: $('#open_edit_pullrequest'),
594 closeButton: $('#close_edit_pullrequest'),
619 closeButton: $('#close_edit_pullrequest'),
595 deleteButton: $('#delete_pullrequest'),
620 deleteButton: $('#delete_pullrequest'),
596 viewFields: $('#pr-desc, #pr-title'),
621 viewFields: $('#pr-desc, #pr-title'),
597 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
622 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
598
623
599 init: function() {
624 init: function() {
600 var that = this;
625 var that = this;
601 this.editButton.on('click', function(e) { that.edit(); });
626 this.editButton.on('click', function(e) { that.edit(); });
602 this.closeButton.on('click', function(e) { that.view(); });
627 this.closeButton.on('click', function(e) { that.view(); });
603 },
628 },
604
629
605 edit: function(event) {
630 edit: function(event) {
606 this.viewFields.hide();
631 this.viewFields.hide();
607 this.editButton.hide();
632 this.editButton.hide();
608 this.deleteButton.hide();
633 this.deleteButton.hide();
609 this.closeButton.show();
634 this.closeButton.show();
610 this.editFields.show();
635 this.editFields.show();
611 codeMirrorInstance.refresh();
636 codeMirrorInstance.refresh();
612 },
637 },
613
638
614 view: function(event) {
639 view: function(event) {
615 this.editButton.show();
640 this.editButton.show();
616 this.deleteButton.show();
641 this.deleteButton.show();
617 this.editFields.hide();
642 this.editFields.hide();
618 this.closeButton.hide();
643 this.closeButton.hide();
619 this.viewFields.show();
644 this.viewFields.show();
620 }
645 }
621 };
646 };
622
647
623 var ReviewersPanel = {
648 var ReviewersPanel = {
624 editButton: $('#open_edit_reviewers'),
649 editButton: $('#open_edit_reviewers'),
625 closeButton: $('#close_edit_reviewers'),
650 closeButton: $('#close_edit_reviewers'),
626 addButton: $('#add_reviewer'),
651 addButton: $('#add_reviewer'),
627 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
652 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
628
653
629 init: function() {
654 init: function() {
630 var self = this;
655 var self = this;
631 this.editButton.on('click', function(e) { self.edit(); });
656 this.editButton.on('click', function(e) { self.edit(); });
632 this.closeButton.on('click', function(e) { self.close(); });
657 this.closeButton.on('click', function(e) { self.close(); });
633 },
658 },
634
659
635 edit: function(event) {
660 edit: function(event) {
636 this.editButton.hide();
661 this.editButton.hide();
637 this.closeButton.show();
662 this.closeButton.show();
638 this.addButton.show();
663 this.addButton.show();
639 this.removeButtons.css('visibility', 'visible');
664 this.removeButtons.css('visibility', 'visible');
640 // review rules
665 // review rules
641 reviewersController.loadReviewRules(
666 reviewersController.loadReviewRules(
642 ${c.pull_request.reviewer_data_json | n});
667 ${c.pull_request.reviewer_data_json | n});
643 },
668 },
644
669
645 close: function(event) {
670 close: function(event) {
646 this.editButton.show();
671 this.editButton.show();
647 this.closeButton.hide();
672 this.closeButton.hide();
648 this.addButton.hide();
673 this.addButton.hide();
649 this.removeButtons.css('visibility', 'hidden');
674 this.removeButtons.css('visibility', 'hidden');
650 // hide review rules
675 // hide review rules
651 reviewersController.hideReviewRules()
676 reviewersController.hideReviewRules()
652 }
677 }
653 };
678 };
654
679
655 PRDetails.init();
680 PRDetails.init();
656 ReviewersPanel.init();
681 ReviewersPanel.init();
657
682
658 showOutdated = function(self){
683 showOutdated = function(self){
659 $('.comment-inline.comment-outdated').show();
684 $('.comment-inline.comment-outdated').show();
660 $('.filediff-outdated').show();
685 $('.filediff-outdated').show();
661 $('.showOutdatedComments').hide();
686 $('.showOutdatedComments').hide();
662 $('.hideOutdatedComments').show();
687 $('.hideOutdatedComments').show();
663 };
688 };
664
689
665 hideOutdated = function(self){
690 hideOutdated = function(self){
666 $('.comment-inline.comment-outdated').hide();
691 $('.comment-inline.comment-outdated').hide();
667 $('.filediff-outdated').hide();
692 $('.filediff-outdated').hide();
668 $('.hideOutdatedComments').hide();
693 $('.hideOutdatedComments').hide();
669 $('.showOutdatedComments').show();
694 $('.showOutdatedComments').show();
670 };
695 };
671
696
672 refreshMergeChecks = function(){
697 refreshMergeChecks = function(){
673 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
698 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
674 $('.pull-request-merge').css('opacity', 0.3);
699 $('.pull-request-merge').css('opacity', 0.3);
675 $('.action-buttons-extra').css('opacity', 0.3);
700 $('.action-buttons-extra').css('opacity', 0.3);
676
701
677 $('.pull-request-merge').load(
702 $('.pull-request-merge').load(
678 loadUrl, function() {
703 loadUrl, function() {
679 $('.pull-request-merge').css('opacity', 1);
704 $('.pull-request-merge').css('opacity', 1);
680
705
681 $('.action-buttons-extra').css('opacity', 1);
706 $('.action-buttons-extra').css('opacity', 1);
682 }
707 }
683 );
708 );
684 };
709 };
685
710
686 closePullRequest = function (status) {
711 closePullRequest = function (status) {
687 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
712 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
688 return false;
713 return false;
689 }
714 }
690 // inject closing flag
715 // inject closing flag
691 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
716 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
692 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
717 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
693 $(generalCommentForm.submitForm).submit();
718 $(generalCommentForm.submitForm).submit();
694 };
719 };
695
720
696 $('#show-outdated-comments').on('click', function(e){
721 $('#show-outdated-comments').on('click', function(e){
697 var button = $(this);
722 var button = $(this);
698 var outdated = $('.comment-outdated');
723 var outdated = $('.comment-outdated');
699
724
700 if (button.html() === "(Show)") {
725 if (button.html() === "(Show)") {
701 button.html("(Hide)");
726 button.html("(Hide)");
702 outdated.show();
727 outdated.show();
703 } else {
728 } else {
704 button.html("(Show)");
729 button.html("(Show)");
705 outdated.hide();
730 outdated.hide();
706 }
731 }
707 });
732 });
708
733
709 $('.show-inline-comments').on('change', function(e){
734 $('.show-inline-comments').on('change', function(e){
710 var show = 'none';
735 var show = 'none';
711 var target = e.currentTarget;
736 var target = e.currentTarget;
712 if(target.checked){
737 if(target.checked){
713 show = ''
738 show = ''
714 }
739 }
715 var boxid = $(target).attr('id_for');
740 var boxid = $(target).attr('id_for');
716 var comments = $('#{0} .inline-comments'.format(boxid));
741 var comments = $('#{0} .inline-comments'.format(boxid));
717 var fn_display = function(idx){
742 var fn_display = function(idx){
718 $(this).css('display', show);
743 $(this).css('display', show);
719 };
744 };
720 $(comments).each(fn_display);
745 $(comments).each(fn_display);
721 var btns = $('#{0} .inline-comments-button'.format(boxid));
746 var btns = $('#{0} .inline-comments-button'.format(boxid));
722 $(btns).each(fn_display);
747 $(btns).each(fn_display);
723 });
748 });
724
749
725 $('#merge_pull_request_form').submit(function() {
750 $('#merge_pull_request_form').submit(function() {
726 if (!$('#merge_pull_request').attr('disabled')) {
751 if (!$('#merge_pull_request').attr('disabled')) {
727 $('#merge_pull_request').attr('disabled', 'disabled');
752 $('#merge_pull_request').attr('disabled', 'disabled');
728 }
753 }
729 return true;
754 return true;
730 });
755 });
731
756
732 $('#edit_pull_request').on('click', function(e){
757 $('#edit_pull_request').on('click', function(e){
733 var title = $('#pr-title-input').val();
758 var title = $('#pr-title-input').val();
734 var description = codeMirrorInstance.getValue();
759 var description = codeMirrorInstance.getValue();
735 var renderer = $('#pr-renderer-input').val();
760 var renderer = $('#pr-renderer-input').val();
736 editPullRequest(
761 editPullRequest(
737 "${c.repo_name}", "${c.pull_request.pull_request_id}",
762 "${c.repo_name}", "${c.pull_request.pull_request_id}",
738 title, description, renderer);
763 title, description, renderer);
739 });
764 });
740
765
741 $('#update_pull_request').on('click', function(e){
766 $('#update_pull_request').on('click', function(e){
742 $(this).attr('disabled', 'disabled');
767 $(this).attr('disabled', 'disabled');
743 $(this).addClass('disabled');
768 $(this).addClass('disabled');
744 $(this).html(_gettext('Saving...'));
769 $(this).html(_gettext('Saving...'));
745 reviewersController.updateReviewers(
770 reviewersController.updateReviewers(
746 "${c.repo_name}", "${c.pull_request.pull_request_id}");
771 "${c.repo_name}", "${c.pull_request.pull_request_id}");
747 });
772 });
748
773
749 $('#update_commits').on('click', function(e){
774
750 var isDisabled = !$(e.currentTarget).attr('disabled');
751 $(e.currentTarget).attr('disabled', 'disabled');
752 $(e.currentTarget).addClass('disabled');
753 $(e.currentTarget).removeClass('btn-primary');
754 $(e.currentTarget).text(_gettext('Updating...'));
755 if(isDisabled){
756 updateCommits(
757 "${c.repo_name}", "${c.pull_request.pull_request_id}");
758 }
759 });
760 // fixing issue with caches on firefox
775 // fixing issue with caches on firefox
761 $('#update_commits').removeAttr("disabled");
776 $('#update_commits').removeAttr("disabled");
762
777
763 $('.show-inline-comments').on('click', function(e){
778 $('.show-inline-comments').on('click', function(e){
764 var boxid = $(this).attr('data-comment-id');
779 var boxid = $(this).attr('data-comment-id');
765 var button = $(this);
780 var button = $(this);
766
781
767 if(button.hasClass("comments-visible")) {
782 if(button.hasClass("comments-visible")) {
768 $('#{0} .inline-comments'.format(boxid)).each(function(index){
783 $('#{0} .inline-comments'.format(boxid)).each(function(index){
769 $(this).hide();
784 $(this).hide();
770 });
785 });
771 button.removeClass("comments-visible");
786 button.removeClass("comments-visible");
772 } else {
787 } else {
773 $('#{0} .inline-comments'.format(boxid)).each(function(index){
788 $('#{0} .inline-comments'.format(boxid)).each(function(index){
774 $(this).show();
789 $(this).show();
775 });
790 });
776 button.addClass("comments-visible");
791 button.addClass("comments-visible");
777 }
792 }
778 });
793 });
779
794
780 // register submit callback on commentForm form to track TODOs
795 // register submit callback on commentForm form to track TODOs
781 window.commentFormGlobalSubmitSuccessCallback = function(){
796 window.commentFormGlobalSubmitSuccessCallback = function(){
782 refreshMergeChecks();
797 refreshMergeChecks();
783 };
798 };
784
799
785 ReviewerAutoComplete('#user');
800 ReviewerAutoComplete('#user');
786
801
787 })
802 })
788
803
789 </script>
804 </script>
790
805
791 </div>
806 </div>
792 </div>
807 </div>
793
808
794 </%def>
809 </%def>
General Comments 0
You need to be logged in to leave comments. Login now