##// END OF EJS Templates
pull-requests: fix deletion of comments and make the test actually test this !...
marcink -
r1978:455f23f1 default
parent child Browse files
Show More
@@ -1,1110 +1,1135 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.tests import (
33 from rhodecode.tests import (
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
56 }[name].format(**kwargs)
56 }[name].format(**kwargs)
57
57
58 if params:
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 return base_url
60 return base_url
61
61
62
62
63 @pytest.mark.usefixtures('app', 'autologin_user')
63 @pytest.mark.usefixtures('app', 'autologin_user')
64 @pytest.mark.backends("git", "hg")
64 @pytest.mark.backends("git", "hg")
65 class TestPullrequestsView(object):
65 class TestPullrequestsView(object):
66
66
67 def test_index(self, backend):
67 def test_index(self, backend):
68 self.app.get(route_path(
68 self.app.get(route_path(
69 'pullrequest_new',
69 'pullrequest_new',
70 repo_name=backend.repo_name))
70 repo_name=backend.repo_name))
71
71
72 def test_option_menu_create_pull_request_exists(self, backend):
72 def test_option_menu_create_pull_request_exists(self, backend):
73 repo_name = backend.repo_name
73 repo_name = backend.repo_name
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75
75
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 'pullrequest_new', repo_name=repo_name)
77 'pullrequest_new', repo_name=repo_name)
78 response.mustcontain(create_pr_link)
78 response.mustcontain(create_pr_link)
79
79
80 def test_create_pr_form_with_raw_commit_id(self, backend):
80 def test_create_pr_form_with_raw_commit_id(self, backend):
81 repo = backend.repo
81 repo = backend.repo
82
82
83 self.app.get(
83 self.app.get(
84 route_path('pullrequest_new',
84 route_path('pullrequest_new',
85 repo_name=repo.repo_name,
85 repo_name=repo.repo_name,
86 commit=repo.get_commit().raw_id),
86 commit=repo.get_commit().raw_id),
87 status=200)
87 status=200)
88
88
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 def test_show(self, pr_util, pr_merge_enabled):
90 def test_show(self, pr_util, pr_merge_enabled):
91 pull_request = pr_util.create_pull_request(
91 pull_request = pr_util.create_pull_request(
92 mergeable=pr_merge_enabled, enable_notifications=False)
92 mergeable=pr_merge_enabled, enable_notifications=False)
93
93
94 response = self.app.get(route_path(
94 response = self.app.get(route_path(
95 'pullrequest_show',
95 'pullrequest_show',
96 repo_name=pull_request.target_repo.scm_instance().name,
96 repo_name=pull_request.target_repo.scm_instance().name,
97 pull_request_id=pull_request.pull_request_id))
97 pull_request_id=pull_request.pull_request_id))
98
98
99 for commit_id in pull_request.revisions:
99 for commit_id in pull_request.revisions:
100 response.mustcontain(commit_id)
100 response.mustcontain(commit_id)
101
101
102 assert pull_request.target_ref_parts.type in response
102 assert pull_request.target_ref_parts.type in response
103 assert pull_request.target_ref_parts.name in response
103 assert pull_request.target_ref_parts.name in response
104 target_clone_url = pull_request.target_repo.clone_url()
104 target_clone_url = pull_request.target_repo.clone_url()
105 assert target_clone_url in response
105 assert target_clone_url in response
106
106
107 assert 'class="pull-request-merge"' in response
107 assert 'class="pull-request-merge"' in response
108 assert (
108 assert (
109 'Server-side pull request merging is disabled.'
109 'Server-side pull request merging is disabled.'
110 in response) != pr_merge_enabled
110 in response) != pr_merge_enabled
111
111
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
113 # Logout
113 # Logout
114 response = self.app.post(
114 response = self.app.post(
115 h.route_path('logout'),
115 h.route_path('logout'),
116 params={'csrf_token': csrf_token})
116 params={'csrf_token': csrf_token})
117 # Login as regular user
117 # Login as regular user
118 response = self.app.post(h.route_path('login'),
118 response = self.app.post(h.route_path('login'),
119 {'username': TEST_USER_REGULAR_LOGIN,
119 {'username': TEST_USER_REGULAR_LOGIN,
120 'password': 'test12'})
120 'password': 'test12'})
121
121
122 pull_request = pr_util.create_pull_request(
122 pull_request = pr_util.create_pull_request(
123 author=TEST_USER_REGULAR_LOGIN)
123 author=TEST_USER_REGULAR_LOGIN)
124
124
125 response = self.app.get(route_path(
125 response = self.app.get(route_path(
126 'pullrequest_show',
126 'pullrequest_show',
127 repo_name=pull_request.target_repo.scm_instance().name,
127 repo_name=pull_request.target_repo.scm_instance().name,
128 pull_request_id=pull_request.pull_request_id))
128 pull_request_id=pull_request.pull_request_id))
129
129
130 response.mustcontain('Server-side pull request merging is disabled.')
130 response.mustcontain('Server-side pull request merging is disabled.')
131
131
132 assert_response = response.assert_response()
132 assert_response = response.assert_response()
133 # for regular user without a merge permissions, we don't see it
133 # for regular user without a merge permissions, we don't see it
134 assert_response.no_element_exists('#close-pull-request-action')
134 assert_response.no_element_exists('#close-pull-request-action')
135
135
136 user_util.grant_user_permission_to_repo(
136 user_util.grant_user_permission_to_repo(
137 pull_request.target_repo,
137 pull_request.target_repo,
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
139 'repository.write')
139 'repository.write')
140 response = self.app.get(route_path(
140 response = self.app.get(route_path(
141 'pullrequest_show',
141 'pullrequest_show',
142 repo_name=pull_request.target_repo.scm_instance().name,
142 repo_name=pull_request.target_repo.scm_instance().name,
143 pull_request_id=pull_request.pull_request_id))
143 pull_request_id=pull_request.pull_request_id))
144
144
145 response.mustcontain('Server-side pull request merging is disabled.')
145 response.mustcontain('Server-side pull request merging is disabled.')
146
146
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 # now regular user has a merge permissions, we have CLOSE button
148 # now regular user has a merge permissions, we have CLOSE button
149 assert_response.one_element_exists('#close-pull-request-action')
149 assert_response.one_element_exists('#close-pull-request-action')
150
150
151 def test_show_invalid_commit_id(self, pr_util):
151 def test_show_invalid_commit_id(self, pr_util):
152 # Simulating invalid revisions which will cause a lookup error
152 # Simulating invalid revisions which will cause a lookup error
153 pull_request = pr_util.create_pull_request()
153 pull_request = pr_util.create_pull_request()
154 pull_request.revisions = ['invalid']
154 pull_request.revisions = ['invalid']
155 Session().add(pull_request)
155 Session().add(pull_request)
156 Session().commit()
156 Session().commit()
157
157
158 response = self.app.get(route_path(
158 response = self.app.get(route_path(
159 'pullrequest_show',
159 'pullrequest_show',
160 repo_name=pull_request.target_repo.scm_instance().name,
160 repo_name=pull_request.target_repo.scm_instance().name,
161 pull_request_id=pull_request.pull_request_id))
161 pull_request_id=pull_request.pull_request_id))
162
162
163 for commit_id in pull_request.revisions:
163 for commit_id in pull_request.revisions:
164 response.mustcontain(commit_id)
164 response.mustcontain(commit_id)
165
165
166 def test_show_invalid_source_reference(self, pr_util):
166 def test_show_invalid_source_reference(self, pr_util):
167 pull_request = pr_util.create_pull_request()
167 pull_request = pr_util.create_pull_request()
168 pull_request.source_ref = 'branch:b:invalid'
168 pull_request.source_ref = 'branch:b:invalid'
169 Session().add(pull_request)
169 Session().add(pull_request)
170 Session().commit()
170 Session().commit()
171
171
172 self.app.get(route_path(
172 self.app.get(route_path(
173 'pullrequest_show',
173 'pullrequest_show',
174 repo_name=pull_request.target_repo.scm_instance().name,
174 repo_name=pull_request.target_repo.scm_instance().name,
175 pull_request_id=pull_request.pull_request_id))
175 pull_request_id=pull_request.pull_request_id))
176
176
177 def test_edit_title_description(self, pr_util, csrf_token):
177 def test_edit_title_description(self, pr_util, csrf_token):
178 pull_request = pr_util.create_pull_request()
178 pull_request = pr_util.create_pull_request()
179 pull_request_id = pull_request.pull_request_id
179 pull_request_id = pull_request.pull_request_id
180
180
181 response = self.app.post(
181 response = self.app.post(
182 route_path('pullrequest_update',
182 route_path('pullrequest_update',
183 repo_name=pull_request.target_repo.repo_name,
183 repo_name=pull_request.target_repo.repo_name,
184 pull_request_id=pull_request_id),
184 pull_request_id=pull_request_id),
185 params={
185 params={
186 'edit_pull_request': 'true',
186 'edit_pull_request': 'true',
187 'title': 'New title',
187 'title': 'New title',
188 'description': 'New description',
188 'description': 'New description',
189 'csrf_token': csrf_token})
189 'csrf_token': csrf_token})
190
190
191 assert_session_flash(
191 assert_session_flash(
192 response, u'Pull request title & description updated.',
192 response, u'Pull request title & description updated.',
193 category='success')
193 category='success')
194
194
195 pull_request = PullRequest.get(pull_request_id)
195 pull_request = PullRequest.get(pull_request_id)
196 assert pull_request.title == 'New title'
196 assert pull_request.title == 'New title'
197 assert pull_request.description == 'New description'
197 assert pull_request.description == 'New description'
198
198
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
200 pull_request = pr_util.create_pull_request()
200 pull_request = pr_util.create_pull_request()
201 pull_request_id = pull_request.pull_request_id
201 pull_request_id = pull_request.pull_request_id
202 pr_util.close()
202 pr_util.close()
203
203
204 response = self.app.post(
204 response = self.app.post(
205 route_path('pullrequest_update',
205 route_path('pullrequest_update',
206 repo_name=pull_request.target_repo.repo_name,
206 repo_name=pull_request.target_repo.repo_name,
207 pull_request_id=pull_request_id),
207 pull_request_id=pull_request_id),
208 params={
208 params={
209 'edit_pull_request': 'true',
209 'edit_pull_request': 'true',
210 'title': 'New title',
210 'title': 'New title',
211 'description': 'New description',
211 'description': 'New description',
212 'csrf_token': csrf_token})
212 'csrf_token': csrf_token})
213
213
214 assert_session_flash(
214 assert_session_flash(
215 response, u'Cannot update closed pull requests.',
215 response, u'Cannot update closed pull requests.',
216 category='error')
216 category='error')
217
217
218 def test_update_invalid_source_reference(self, pr_util, csrf_token):
218 def test_update_invalid_source_reference(self, pr_util, csrf_token):
219 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
219 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
220
220
221 pull_request = pr_util.create_pull_request()
221 pull_request = pr_util.create_pull_request()
222 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
222 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
223 Session().add(pull_request)
223 Session().add(pull_request)
224 Session().commit()
224 Session().commit()
225
225
226 pull_request_id = pull_request.pull_request_id
226 pull_request_id = pull_request.pull_request_id
227
227
228 response = self.app.post(
228 response = self.app.post(
229 route_path('pullrequest_update',
229 route_path('pullrequest_update',
230 repo_name=pull_request.target_repo.repo_name,
230 repo_name=pull_request.target_repo.repo_name,
231 pull_request_id=pull_request_id),
231 pull_request_id=pull_request_id),
232 params={'update_commits': 'true',
232 params={'update_commits': 'true',
233 'csrf_token': csrf_token})
233 'csrf_token': csrf_token})
234
234
235 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
235 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
236 UpdateFailureReason.MISSING_SOURCE_REF]
236 UpdateFailureReason.MISSING_SOURCE_REF]
237 assert_session_flash(response, expected_msg, category='error')
237 assert_session_flash(response, expected_msg, category='error')
238
238
239 def test_missing_target_reference(self, pr_util, csrf_token):
239 def test_missing_target_reference(self, pr_util, csrf_token):
240 from rhodecode.lib.vcs.backends.base import MergeFailureReason
240 from rhodecode.lib.vcs.backends.base import MergeFailureReason
241 pull_request = pr_util.create_pull_request(
241 pull_request = pr_util.create_pull_request(
242 approved=True, mergeable=True)
242 approved=True, mergeable=True)
243 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
243 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
244 Session().add(pull_request)
244 Session().add(pull_request)
245 Session().commit()
245 Session().commit()
246
246
247 pull_request_id = pull_request.pull_request_id
247 pull_request_id = pull_request.pull_request_id
248 pull_request_url = route_path(
248 pull_request_url = route_path(
249 'pullrequest_show',
249 'pullrequest_show',
250 repo_name=pull_request.target_repo.repo_name,
250 repo_name=pull_request.target_repo.repo_name,
251 pull_request_id=pull_request_id)
251 pull_request_id=pull_request_id)
252
252
253 response = self.app.get(pull_request_url)
253 response = self.app.get(pull_request_url)
254
254
255 assertr = AssertResponse(response)
255 assertr = AssertResponse(response)
256 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
256 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
257 MergeFailureReason.MISSING_TARGET_REF]
257 MergeFailureReason.MISSING_TARGET_REF]
258 assertr.element_contains(
258 assertr.element_contains(
259 'span[data-role="merge-message"]', str(expected_msg))
259 'span[data-role="merge-message"]', str(expected_msg))
260
260
261 def test_comment_and_close_pull_request_custom_message_approved(
261 def test_comment_and_close_pull_request_custom_message_approved(
262 self, pr_util, csrf_token, xhr_header):
262 self, pr_util, csrf_token, xhr_header):
263
263
264 pull_request = pr_util.create_pull_request(approved=True)
264 pull_request = pr_util.create_pull_request(approved=True)
265 pull_request_id = pull_request.pull_request_id
265 pull_request_id = pull_request.pull_request_id
266 author = pull_request.user_id
266 author = pull_request.user_id
267 repo = pull_request.target_repo.repo_id
267 repo = pull_request.target_repo.repo_id
268
268
269 self.app.post(
269 self.app.post(
270 route_path('pullrequest_comment_create',
270 route_path('pullrequest_comment_create',
271 repo_name=pull_request.target_repo.scm_instance().name,
271 repo_name=pull_request.target_repo.scm_instance().name,
272 pull_request_id=pull_request_id),
272 pull_request_id=pull_request_id),
273 params={
273 params={
274 'close_pull_request': '1',
274 'close_pull_request': '1',
275 'text': 'Closing a PR',
275 'text': 'Closing a PR',
276 'csrf_token': csrf_token},
276 'csrf_token': csrf_token},
277 extra_environ=xhr_header,)
277 extra_environ=xhr_header,)
278
278
279 journal = UserLog.query()\
279 journal = UserLog.query()\
280 .filter(UserLog.user_id == author)\
280 .filter(UserLog.user_id == author)\
281 .filter(UserLog.repository_id == repo) \
281 .filter(UserLog.repository_id == repo) \
282 .order_by('user_log_id') \
282 .order_by('user_log_id') \
283 .all()
283 .all()
284 assert journal[-1].action == 'repo.pull_request.close'
284 assert journal[-1].action == 'repo.pull_request.close'
285
285
286 pull_request = PullRequest.get(pull_request_id)
286 pull_request = PullRequest.get(pull_request_id)
287 assert pull_request.is_closed()
287 assert pull_request.is_closed()
288
288
289 status = ChangesetStatusModel().get_status(
289 status = ChangesetStatusModel().get_status(
290 pull_request.source_repo, pull_request=pull_request)
290 pull_request.source_repo, pull_request=pull_request)
291 assert status == ChangesetStatus.STATUS_APPROVED
291 assert status == ChangesetStatus.STATUS_APPROVED
292 comments = ChangesetComment().query() \
292 comments = ChangesetComment().query() \
293 .filter(ChangesetComment.pull_request == pull_request) \
293 .filter(ChangesetComment.pull_request == pull_request) \
294 .order_by(ChangesetComment.comment_id.asc())\
294 .order_by(ChangesetComment.comment_id.asc())\
295 .all()
295 .all()
296 assert comments[-1].text == 'Closing a PR'
296 assert comments[-1].text == 'Closing a PR'
297
297
298 def test_comment_force_close_pull_request_rejected(
298 def test_comment_force_close_pull_request_rejected(
299 self, pr_util, csrf_token, xhr_header):
299 self, pr_util, csrf_token, xhr_header):
300 pull_request = pr_util.create_pull_request()
300 pull_request = pr_util.create_pull_request()
301 pull_request_id = pull_request.pull_request_id
301 pull_request_id = pull_request.pull_request_id
302 PullRequestModel().update_reviewers(
302 PullRequestModel().update_reviewers(
303 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
303 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
304 pull_request.author)
304 pull_request.author)
305 author = pull_request.user_id
305 author = pull_request.user_id
306 repo = pull_request.target_repo.repo_id
306 repo = pull_request.target_repo.repo_id
307
307
308 self.app.post(
308 self.app.post(
309 route_path('pullrequest_comment_create',
309 route_path('pullrequest_comment_create',
310 repo_name=pull_request.target_repo.scm_instance().name,
310 repo_name=pull_request.target_repo.scm_instance().name,
311 pull_request_id=pull_request_id),
311 pull_request_id=pull_request_id),
312 params={
312 params={
313 'close_pull_request': '1',
313 'close_pull_request': '1',
314 'csrf_token': csrf_token},
314 'csrf_token': csrf_token},
315 extra_environ=xhr_header)
315 extra_environ=xhr_header)
316
316
317 pull_request = PullRequest.get(pull_request_id)
317 pull_request = PullRequest.get(pull_request_id)
318
318
319 journal = UserLog.query()\
319 journal = UserLog.query()\
320 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
320 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
321 .order_by('user_log_id') \
321 .order_by('user_log_id') \
322 .all()
322 .all()
323 assert journal[-1].action == 'repo.pull_request.close'
323 assert journal[-1].action == 'repo.pull_request.close'
324
324
325 # check only the latest status, not the review status
325 # check only the latest status, not the review status
326 status = ChangesetStatusModel().get_status(
326 status = ChangesetStatusModel().get_status(
327 pull_request.source_repo, pull_request=pull_request)
327 pull_request.source_repo, pull_request=pull_request)
328 assert status == ChangesetStatus.STATUS_REJECTED
328 assert status == ChangesetStatus.STATUS_REJECTED
329
329
330 def test_comment_and_close_pull_request(
330 def test_comment_and_close_pull_request(
331 self, pr_util, csrf_token, xhr_header):
331 self, pr_util, csrf_token, xhr_header):
332 pull_request = pr_util.create_pull_request()
332 pull_request = pr_util.create_pull_request()
333 pull_request_id = pull_request.pull_request_id
333 pull_request_id = pull_request.pull_request_id
334
334
335 response = self.app.post(
335 response = self.app.post(
336 route_path('pullrequest_comment_create',
336 route_path('pullrequest_comment_create',
337 repo_name=pull_request.target_repo.scm_instance().name,
337 repo_name=pull_request.target_repo.scm_instance().name,
338 pull_request_id=pull_request.pull_request_id),
338 pull_request_id=pull_request.pull_request_id),
339 params={
339 params={
340 'close_pull_request': 'true',
340 'close_pull_request': 'true',
341 'csrf_token': csrf_token},
341 'csrf_token': csrf_token},
342 extra_environ=xhr_header)
342 extra_environ=xhr_header)
343
343
344 assert response.json
344 assert response.json
345
345
346 pull_request = PullRequest.get(pull_request_id)
346 pull_request = PullRequest.get(pull_request_id)
347 assert pull_request.is_closed()
347 assert pull_request.is_closed()
348
348
349 # check only the latest status, not the review status
349 # check only the latest status, not the review status
350 status = ChangesetStatusModel().get_status(
350 status = ChangesetStatusModel().get_status(
351 pull_request.source_repo, pull_request=pull_request)
351 pull_request.source_repo, pull_request=pull_request)
352 assert status == ChangesetStatus.STATUS_REJECTED
352 assert status == ChangesetStatus.STATUS_REJECTED
353
353
354 def test_create_pull_request(self, backend, csrf_token):
354 def test_create_pull_request(self, backend, csrf_token):
355 commits = [
355 commits = [
356 {'message': 'ancestor'},
356 {'message': 'ancestor'},
357 {'message': 'change'},
357 {'message': 'change'},
358 {'message': 'change2'},
358 {'message': 'change2'},
359 ]
359 ]
360 commit_ids = backend.create_master_repo(commits)
360 commit_ids = backend.create_master_repo(commits)
361 target = backend.create_repo(heads=['ancestor'])
361 target = backend.create_repo(heads=['ancestor'])
362 source = backend.create_repo(heads=['change2'])
362 source = backend.create_repo(heads=['change2'])
363
363
364 response = self.app.post(
364 response = self.app.post(
365 route_path('pullrequest_create', repo_name=source.repo_name),
365 route_path('pullrequest_create', repo_name=source.repo_name),
366 [
366 [
367 ('source_repo', source.repo_name),
367 ('source_repo', source.repo_name),
368 ('source_ref', 'branch:default:' + commit_ids['change2']),
368 ('source_ref', 'branch:default:' + commit_ids['change2']),
369 ('target_repo', target.repo_name),
369 ('target_repo', target.repo_name),
370 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
370 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
371 ('common_ancestor', commit_ids['ancestor']),
371 ('common_ancestor', commit_ids['ancestor']),
372 ('pullrequest_desc', 'Description'),
372 ('pullrequest_desc', 'Description'),
373 ('pullrequest_title', 'Title'),
373 ('pullrequest_title', 'Title'),
374 ('__start__', 'review_members:sequence'),
374 ('__start__', 'review_members:sequence'),
375 ('__start__', 'reviewer:mapping'),
375 ('__start__', 'reviewer:mapping'),
376 ('user_id', '1'),
376 ('user_id', '1'),
377 ('__start__', 'reasons:sequence'),
377 ('__start__', 'reasons:sequence'),
378 ('reason', 'Some reason'),
378 ('reason', 'Some reason'),
379 ('__end__', 'reasons:sequence'),
379 ('__end__', 'reasons:sequence'),
380 ('mandatory', 'False'),
380 ('mandatory', 'False'),
381 ('__end__', 'reviewer:mapping'),
381 ('__end__', 'reviewer:mapping'),
382 ('__end__', 'review_members:sequence'),
382 ('__end__', 'review_members:sequence'),
383 ('__start__', 'revisions:sequence'),
383 ('__start__', 'revisions:sequence'),
384 ('revisions', commit_ids['change']),
384 ('revisions', commit_ids['change']),
385 ('revisions', commit_ids['change2']),
385 ('revisions', commit_ids['change2']),
386 ('__end__', 'revisions:sequence'),
386 ('__end__', 'revisions:sequence'),
387 ('user', ''),
387 ('user', ''),
388 ('csrf_token', csrf_token),
388 ('csrf_token', csrf_token),
389 ],
389 ],
390 status=302)
390 status=302)
391
391
392 location = response.headers['Location']
392 location = response.headers['Location']
393 pull_request_id = location.rsplit('/', 1)[1]
393 pull_request_id = location.rsplit('/', 1)[1]
394 assert pull_request_id != 'new'
394 assert pull_request_id != 'new'
395 pull_request = PullRequest.get(int(pull_request_id))
395 pull_request = PullRequest.get(int(pull_request_id))
396
396
397 # check that we have now both revisions
397 # check that we have now both revisions
398 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
398 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
399 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
399 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
400 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
400 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
401 assert pull_request.target_ref == expected_target_ref
401 assert pull_request.target_ref == expected_target_ref
402
402
403 def test_reviewer_notifications(self, backend, csrf_token):
403 def test_reviewer_notifications(self, backend, csrf_token):
404 # We have to use the app.post for this test so it will create the
404 # We have to use the app.post for this test so it will create the
405 # notifications properly with the new PR
405 # notifications properly with the new PR
406 commits = [
406 commits = [
407 {'message': 'ancestor',
407 {'message': 'ancestor',
408 'added': [FileNode('file_A', content='content_of_ancestor')]},
408 'added': [FileNode('file_A', content='content_of_ancestor')]},
409 {'message': 'change',
409 {'message': 'change',
410 'added': [FileNode('file_a', content='content_of_change')]},
410 'added': [FileNode('file_a', content='content_of_change')]},
411 {'message': 'change-child'},
411 {'message': 'change-child'},
412 {'message': 'ancestor-child', 'parents': ['ancestor'],
412 {'message': 'ancestor-child', 'parents': ['ancestor'],
413 'added': [
413 'added': [
414 FileNode('file_B', content='content_of_ancestor_child')]},
414 FileNode('file_B', content='content_of_ancestor_child')]},
415 {'message': 'ancestor-child-2'},
415 {'message': 'ancestor-child-2'},
416 ]
416 ]
417 commit_ids = backend.create_master_repo(commits)
417 commit_ids = backend.create_master_repo(commits)
418 target = backend.create_repo(heads=['ancestor-child'])
418 target = backend.create_repo(heads=['ancestor-child'])
419 source = backend.create_repo(heads=['change'])
419 source = backend.create_repo(heads=['change'])
420
420
421 response = self.app.post(
421 response = self.app.post(
422 route_path('pullrequest_create', repo_name=source.repo_name),
422 route_path('pullrequest_create', repo_name=source.repo_name),
423 [
423 [
424 ('source_repo', source.repo_name),
424 ('source_repo', source.repo_name),
425 ('source_ref', 'branch:default:' + commit_ids['change']),
425 ('source_ref', 'branch:default:' + commit_ids['change']),
426 ('target_repo', target.repo_name),
426 ('target_repo', target.repo_name),
427 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
427 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
428 ('common_ancestor', commit_ids['ancestor']),
428 ('common_ancestor', commit_ids['ancestor']),
429 ('pullrequest_desc', 'Description'),
429 ('pullrequest_desc', 'Description'),
430 ('pullrequest_title', 'Title'),
430 ('pullrequest_title', 'Title'),
431 ('__start__', 'review_members:sequence'),
431 ('__start__', 'review_members:sequence'),
432 ('__start__', 'reviewer:mapping'),
432 ('__start__', 'reviewer:mapping'),
433 ('user_id', '2'),
433 ('user_id', '2'),
434 ('__start__', 'reasons:sequence'),
434 ('__start__', 'reasons:sequence'),
435 ('reason', 'Some reason'),
435 ('reason', 'Some reason'),
436 ('__end__', 'reasons:sequence'),
436 ('__end__', 'reasons:sequence'),
437 ('mandatory', 'False'),
437 ('mandatory', 'False'),
438 ('__end__', 'reviewer:mapping'),
438 ('__end__', 'reviewer:mapping'),
439 ('__end__', 'review_members:sequence'),
439 ('__end__', 'review_members:sequence'),
440 ('__start__', 'revisions:sequence'),
440 ('__start__', 'revisions:sequence'),
441 ('revisions', commit_ids['change']),
441 ('revisions', commit_ids['change']),
442 ('__end__', 'revisions:sequence'),
442 ('__end__', 'revisions:sequence'),
443 ('user', ''),
443 ('user', ''),
444 ('csrf_token', csrf_token),
444 ('csrf_token', csrf_token),
445 ],
445 ],
446 status=302)
446 status=302)
447
447
448 location = response.headers['Location']
448 location = response.headers['Location']
449
449
450 pull_request_id = location.rsplit('/', 1)[1]
450 pull_request_id = location.rsplit('/', 1)[1]
451 assert pull_request_id != 'new'
451 assert pull_request_id != 'new'
452 pull_request = PullRequest.get(int(pull_request_id))
452 pull_request = PullRequest.get(int(pull_request_id))
453
453
454 # Check that a notification was made
454 # Check that a notification was made
455 notifications = Notification.query()\
455 notifications = Notification.query()\
456 .filter(Notification.created_by == pull_request.author.user_id,
456 .filter(Notification.created_by == pull_request.author.user_id,
457 Notification.type_ == Notification.TYPE_PULL_REQUEST,
457 Notification.type_ == Notification.TYPE_PULL_REQUEST,
458 Notification.subject.contains(
458 Notification.subject.contains(
459 "wants you to review pull request #%s" % pull_request_id))
459 "wants you to review pull request #%s" % pull_request_id))
460 assert len(notifications.all()) == 1
460 assert len(notifications.all()) == 1
461
461
462 # Change reviewers and check that a notification was made
462 # Change reviewers and check that a notification was made
463 PullRequestModel().update_reviewers(
463 PullRequestModel().update_reviewers(
464 pull_request.pull_request_id, [(1, [], False)],
464 pull_request.pull_request_id, [(1, [], False)],
465 pull_request.author)
465 pull_request.author)
466 assert len(notifications.all()) == 2
466 assert len(notifications.all()) == 2
467
467
468 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
468 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
469 csrf_token):
469 csrf_token):
470 commits = [
470 commits = [
471 {'message': 'ancestor',
471 {'message': 'ancestor',
472 'added': [FileNode('file_A', content='content_of_ancestor')]},
472 'added': [FileNode('file_A', content='content_of_ancestor')]},
473 {'message': 'change',
473 {'message': 'change',
474 'added': [FileNode('file_a', content='content_of_change')]},
474 'added': [FileNode('file_a', content='content_of_change')]},
475 {'message': 'change-child'},
475 {'message': 'change-child'},
476 {'message': 'ancestor-child', 'parents': ['ancestor'],
476 {'message': 'ancestor-child', 'parents': ['ancestor'],
477 'added': [
477 'added': [
478 FileNode('file_B', content='content_of_ancestor_child')]},
478 FileNode('file_B', content='content_of_ancestor_child')]},
479 {'message': 'ancestor-child-2'},
479 {'message': 'ancestor-child-2'},
480 ]
480 ]
481 commit_ids = backend.create_master_repo(commits)
481 commit_ids = backend.create_master_repo(commits)
482 target = backend.create_repo(heads=['ancestor-child'])
482 target = backend.create_repo(heads=['ancestor-child'])
483 source = backend.create_repo(heads=['change'])
483 source = backend.create_repo(heads=['change'])
484
484
485 response = self.app.post(
485 response = self.app.post(
486 route_path('pullrequest_create', repo_name=source.repo_name),
486 route_path('pullrequest_create', repo_name=source.repo_name),
487 [
487 [
488 ('source_repo', source.repo_name),
488 ('source_repo', source.repo_name),
489 ('source_ref', 'branch:default:' + commit_ids['change']),
489 ('source_ref', 'branch:default:' + commit_ids['change']),
490 ('target_repo', target.repo_name),
490 ('target_repo', target.repo_name),
491 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
491 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
492 ('common_ancestor', commit_ids['ancestor']),
492 ('common_ancestor', commit_ids['ancestor']),
493 ('pullrequest_desc', 'Description'),
493 ('pullrequest_desc', 'Description'),
494 ('pullrequest_title', 'Title'),
494 ('pullrequest_title', 'Title'),
495 ('__start__', 'review_members:sequence'),
495 ('__start__', 'review_members:sequence'),
496 ('__start__', 'reviewer:mapping'),
496 ('__start__', 'reviewer:mapping'),
497 ('user_id', '1'),
497 ('user_id', '1'),
498 ('__start__', 'reasons:sequence'),
498 ('__start__', 'reasons:sequence'),
499 ('reason', 'Some reason'),
499 ('reason', 'Some reason'),
500 ('__end__', 'reasons:sequence'),
500 ('__end__', 'reasons:sequence'),
501 ('mandatory', 'False'),
501 ('mandatory', 'False'),
502 ('__end__', 'reviewer:mapping'),
502 ('__end__', 'reviewer:mapping'),
503 ('__end__', 'review_members:sequence'),
503 ('__end__', 'review_members:sequence'),
504 ('__start__', 'revisions:sequence'),
504 ('__start__', 'revisions:sequence'),
505 ('revisions', commit_ids['change']),
505 ('revisions', commit_ids['change']),
506 ('__end__', 'revisions:sequence'),
506 ('__end__', 'revisions:sequence'),
507 ('user', ''),
507 ('user', ''),
508 ('csrf_token', csrf_token),
508 ('csrf_token', csrf_token),
509 ],
509 ],
510 status=302)
510 status=302)
511
511
512 location = response.headers['Location']
512 location = response.headers['Location']
513
513
514 pull_request_id = location.rsplit('/', 1)[1]
514 pull_request_id = location.rsplit('/', 1)[1]
515 assert pull_request_id != 'new'
515 assert pull_request_id != 'new'
516 pull_request = PullRequest.get(int(pull_request_id))
516 pull_request = PullRequest.get(int(pull_request_id))
517
517
518 # target_ref has to point to the ancestor's commit_id in order to
518 # target_ref has to point to the ancestor's commit_id in order to
519 # show the correct diff
519 # show the correct diff
520 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
520 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
521 assert pull_request.target_ref == expected_target_ref
521 assert pull_request.target_ref == expected_target_ref
522
522
523 # Check generated diff contents
523 # Check generated diff contents
524 response = response.follow()
524 response = response.follow()
525 assert 'content_of_ancestor' not in response.body
525 assert 'content_of_ancestor' not in response.body
526 assert 'content_of_ancestor-child' not in response.body
526 assert 'content_of_ancestor-child' not in response.body
527 assert 'content_of_change' in response.body
527 assert 'content_of_change' in response.body
528
528
529 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
529 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
530 # Clear any previous calls to rcextensions
530 # Clear any previous calls to rcextensions
531 rhodecode.EXTENSIONS.calls.clear()
531 rhodecode.EXTENSIONS.calls.clear()
532
532
533 pull_request = pr_util.create_pull_request(
533 pull_request = pr_util.create_pull_request(
534 approved=True, mergeable=True)
534 approved=True, mergeable=True)
535 pull_request_id = pull_request.pull_request_id
535 pull_request_id = pull_request.pull_request_id
536 repo_name = pull_request.target_repo.scm_instance().name,
536 repo_name = pull_request.target_repo.scm_instance().name,
537
537
538 response = self.app.post(
538 response = self.app.post(
539 route_path('pullrequest_merge',
539 route_path('pullrequest_merge',
540 repo_name=str(repo_name[0]),
540 repo_name=str(repo_name[0]),
541 pull_request_id=pull_request_id),
541 pull_request_id=pull_request_id),
542 params={'csrf_token': csrf_token}).follow()
542 params={'csrf_token': csrf_token}).follow()
543
543
544 pull_request = PullRequest.get(pull_request_id)
544 pull_request = PullRequest.get(pull_request_id)
545
545
546 assert response.status_int == 200
546 assert response.status_int == 200
547 assert pull_request.is_closed()
547 assert pull_request.is_closed()
548 assert_pull_request_status(
548 assert_pull_request_status(
549 pull_request, ChangesetStatus.STATUS_APPROVED)
549 pull_request, ChangesetStatus.STATUS_APPROVED)
550
550
551 # Check the relevant log entries were added
551 # Check the relevant log entries were added
552 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
552 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
553 actions = [log.action for log in user_logs]
553 actions = [log.action for log in user_logs]
554 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
554 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
555 expected_actions = [
555 expected_actions = [
556 u'repo.pull_request.close',
556 u'repo.pull_request.close',
557 u'repo.pull_request.merge',
557 u'repo.pull_request.merge',
558 u'repo.pull_request.comment.create'
558 u'repo.pull_request.comment.create'
559 ]
559 ]
560 assert actions == expected_actions
560 assert actions == expected_actions
561
561
562 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
562 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
563 actions = [log for log in user_logs]
563 actions = [log for log in user_logs]
564 assert actions[-1].action == 'user.push'
564 assert actions[-1].action == 'user.push'
565 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
565 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
566
566
567 # Check post_push rcextension was really executed
567 # Check post_push rcextension was really executed
568 push_calls = rhodecode.EXTENSIONS.calls['post_push']
568 push_calls = rhodecode.EXTENSIONS.calls['post_push']
569 assert len(push_calls) == 1
569 assert len(push_calls) == 1
570 unused_last_call_args, last_call_kwargs = push_calls[0]
570 unused_last_call_args, last_call_kwargs = push_calls[0]
571 assert last_call_kwargs['action'] == 'push'
571 assert last_call_kwargs['action'] == 'push'
572 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
572 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
573
573
574 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
574 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
575 pull_request = pr_util.create_pull_request(mergeable=False)
575 pull_request = pr_util.create_pull_request(mergeable=False)
576 pull_request_id = pull_request.pull_request_id
576 pull_request_id = pull_request.pull_request_id
577 pull_request = PullRequest.get(pull_request_id)
577 pull_request = PullRequest.get(pull_request_id)
578
578
579 response = self.app.post(
579 response = self.app.post(
580 route_path('pullrequest_merge',
580 route_path('pullrequest_merge',
581 repo_name=pull_request.target_repo.scm_instance().name,
581 repo_name=pull_request.target_repo.scm_instance().name,
582 pull_request_id=pull_request.pull_request_id),
582 pull_request_id=pull_request.pull_request_id),
583 params={'csrf_token': csrf_token}).follow()
583 params={'csrf_token': csrf_token}).follow()
584
584
585 assert response.status_int == 200
585 assert response.status_int == 200
586 response.mustcontain(
586 response.mustcontain(
587 'Merge is not currently possible because of below failed checks.')
587 'Merge is not currently possible because of below failed checks.')
588 response.mustcontain('Server-side pull request merging is disabled.')
588 response.mustcontain('Server-side pull request merging is disabled.')
589
589
590 @pytest.mark.skip_backends('svn')
590 @pytest.mark.skip_backends('svn')
591 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
591 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
592 pull_request = pr_util.create_pull_request(mergeable=True)
592 pull_request = pr_util.create_pull_request(mergeable=True)
593 pull_request_id = pull_request.pull_request_id
593 pull_request_id = pull_request.pull_request_id
594 repo_name = pull_request.target_repo.scm_instance().name
594 repo_name = pull_request.target_repo.scm_instance().name
595
595
596 response = self.app.post(
596 response = self.app.post(
597 route_path('pullrequest_merge',
597 route_path('pullrequest_merge',
598 repo_name=repo_name,
598 repo_name=repo_name,
599 pull_request_id=pull_request_id),
599 pull_request_id=pull_request_id),
600 params={'csrf_token': csrf_token}).follow()
600 params={'csrf_token': csrf_token}).follow()
601
601
602 assert response.status_int == 200
602 assert response.status_int == 200
603
603
604 response.mustcontain(
604 response.mustcontain(
605 'Merge is not currently possible because of below failed checks.')
605 'Merge is not currently possible because of below failed checks.')
606 response.mustcontain('Pull request reviewer approval is pending.')
606 response.mustcontain('Pull request reviewer approval is pending.')
607
607
608 def test_merge_pull_request_renders_failure_reason(
608 def test_merge_pull_request_renders_failure_reason(
609 self, user_regular, csrf_token, pr_util):
609 self, user_regular, csrf_token, pr_util):
610 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
610 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
611 pull_request_id = pull_request.pull_request_id
611 pull_request_id = pull_request.pull_request_id
612 repo_name = pull_request.target_repo.scm_instance().name
612 repo_name = pull_request.target_repo.scm_instance().name
613
613
614 model_patcher = mock.patch.multiple(
614 model_patcher = mock.patch.multiple(
615 PullRequestModel,
615 PullRequestModel,
616 merge=mock.Mock(return_value=MergeResponse(
616 merge=mock.Mock(return_value=MergeResponse(
617 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
617 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
618 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
618 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
619
619
620 with model_patcher:
620 with model_patcher:
621 response = self.app.post(
621 response = self.app.post(
622 route_path('pullrequest_merge',
622 route_path('pullrequest_merge',
623 repo_name=repo_name,
623 repo_name=repo_name,
624 pull_request_id=pull_request_id),
624 pull_request_id=pull_request_id),
625 params={'csrf_token': csrf_token}, status=302)
625 params={'csrf_token': csrf_token}, status=302)
626
626
627 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
627 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
628 MergeFailureReason.PUSH_FAILED])
628 MergeFailureReason.PUSH_FAILED])
629
629
630 def test_update_source_revision(self, backend, csrf_token):
630 def test_update_source_revision(self, backend, csrf_token):
631 commits = [
631 commits = [
632 {'message': 'ancestor'},
632 {'message': 'ancestor'},
633 {'message': 'change'},
633 {'message': 'change'},
634 {'message': 'change-2'},
634 {'message': 'change-2'},
635 ]
635 ]
636 commit_ids = backend.create_master_repo(commits)
636 commit_ids = backend.create_master_repo(commits)
637 target = backend.create_repo(heads=['ancestor'])
637 target = backend.create_repo(heads=['ancestor'])
638 source = backend.create_repo(heads=['change'])
638 source = backend.create_repo(heads=['change'])
639
639
640 # create pr from a in source to A in target
640 # create pr from a in source to A in target
641 pull_request = PullRequest()
641 pull_request = PullRequest()
642 pull_request.source_repo = source
642 pull_request.source_repo = source
643 # TODO: johbo: Make sure that we write the source ref this way!
643 # TODO: johbo: Make sure that we write the source ref this way!
644 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
644 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
645 branch=backend.default_branch_name, commit_id=commit_ids['change'])
645 branch=backend.default_branch_name, commit_id=commit_ids['change'])
646 pull_request.target_repo = target
646 pull_request.target_repo = target
647
647
648 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
648 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
649 branch=backend.default_branch_name,
649 branch=backend.default_branch_name,
650 commit_id=commit_ids['ancestor'])
650 commit_id=commit_ids['ancestor'])
651 pull_request.revisions = [commit_ids['change']]
651 pull_request.revisions = [commit_ids['change']]
652 pull_request.title = u"Test"
652 pull_request.title = u"Test"
653 pull_request.description = u"Description"
653 pull_request.description = u"Description"
654 pull_request.author = UserModel().get_by_username(
654 pull_request.author = UserModel().get_by_username(
655 TEST_USER_ADMIN_LOGIN)
655 TEST_USER_ADMIN_LOGIN)
656 Session().add(pull_request)
656 Session().add(pull_request)
657 Session().commit()
657 Session().commit()
658 pull_request_id = pull_request.pull_request_id
658 pull_request_id = pull_request.pull_request_id
659
659
660 # source has ancestor - change - change-2
660 # source has ancestor - change - change-2
661 backend.pull_heads(source, heads=['change-2'])
661 backend.pull_heads(source, heads=['change-2'])
662
662
663 # update PR
663 # update PR
664 self.app.post(
664 self.app.post(
665 route_path('pullrequest_update',
665 route_path('pullrequest_update',
666 repo_name=target.repo_name,
666 repo_name=target.repo_name,
667 pull_request_id=pull_request_id),
667 pull_request_id=pull_request_id),
668 params={'update_commits': 'true',
668 params={'update_commits': 'true',
669 'csrf_token': csrf_token})
669 'csrf_token': csrf_token})
670
670
671 # check that we have now both revisions
671 # check that we have now both revisions
672 pull_request = PullRequest.get(pull_request_id)
672 pull_request = PullRequest.get(pull_request_id)
673 assert pull_request.revisions == [
673 assert pull_request.revisions == [
674 commit_ids['change-2'], commit_ids['change']]
674 commit_ids['change-2'], commit_ids['change']]
675
675
676 # TODO: johbo: this should be a test on its own
676 # TODO: johbo: this should be a test on its own
677 response = self.app.get(route_path(
677 response = self.app.get(route_path(
678 'pullrequest_new',
678 'pullrequest_new',
679 repo_name=target.repo_name))
679 repo_name=target.repo_name))
680 assert response.status_int == 200
680 assert response.status_int == 200
681 assert 'Pull request updated to' in response.body
681 assert 'Pull request updated to' in response.body
682 assert 'with 1 added, 0 removed commits.' in response.body
682 assert 'with 1 added, 0 removed commits.' in response.body
683
683
684 def test_update_target_revision(self, backend, csrf_token):
684 def test_update_target_revision(self, backend, csrf_token):
685 commits = [
685 commits = [
686 {'message': 'ancestor'},
686 {'message': 'ancestor'},
687 {'message': 'change'},
687 {'message': 'change'},
688 {'message': 'ancestor-new', 'parents': ['ancestor']},
688 {'message': 'ancestor-new', 'parents': ['ancestor']},
689 {'message': 'change-rebased'},
689 {'message': 'change-rebased'},
690 ]
690 ]
691 commit_ids = backend.create_master_repo(commits)
691 commit_ids = backend.create_master_repo(commits)
692 target = backend.create_repo(heads=['ancestor'])
692 target = backend.create_repo(heads=['ancestor'])
693 source = backend.create_repo(heads=['change'])
693 source = backend.create_repo(heads=['change'])
694
694
695 # create pr from a in source to A in target
695 # create pr from a in source to A in target
696 pull_request = PullRequest()
696 pull_request = PullRequest()
697 pull_request.source_repo = source
697 pull_request.source_repo = source
698 # TODO: johbo: Make sure that we write the source ref this way!
698 # TODO: johbo: Make sure that we write the source ref this way!
699 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
699 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
700 branch=backend.default_branch_name, commit_id=commit_ids['change'])
700 branch=backend.default_branch_name, commit_id=commit_ids['change'])
701 pull_request.target_repo = target
701 pull_request.target_repo = target
702 # TODO: johbo: Target ref should be branch based, since tip can jump
702 # TODO: johbo: Target ref should be branch based, since tip can jump
703 # from branch to branch
703 # from branch to branch
704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
705 branch=backend.default_branch_name,
705 branch=backend.default_branch_name,
706 commit_id=commit_ids['ancestor'])
706 commit_id=commit_ids['ancestor'])
707 pull_request.revisions = [commit_ids['change']]
707 pull_request.revisions = [commit_ids['change']]
708 pull_request.title = u"Test"
708 pull_request.title = u"Test"
709 pull_request.description = u"Description"
709 pull_request.description = u"Description"
710 pull_request.author = UserModel().get_by_username(
710 pull_request.author = UserModel().get_by_username(
711 TEST_USER_ADMIN_LOGIN)
711 TEST_USER_ADMIN_LOGIN)
712 Session().add(pull_request)
712 Session().add(pull_request)
713 Session().commit()
713 Session().commit()
714 pull_request_id = pull_request.pull_request_id
714 pull_request_id = pull_request.pull_request_id
715
715
716 # target has ancestor - ancestor-new
716 # target has ancestor - ancestor-new
717 # source has ancestor - ancestor-new - change-rebased
717 # source has ancestor - ancestor-new - change-rebased
718 backend.pull_heads(target, heads=['ancestor-new'])
718 backend.pull_heads(target, heads=['ancestor-new'])
719 backend.pull_heads(source, heads=['change-rebased'])
719 backend.pull_heads(source, heads=['change-rebased'])
720
720
721 # update PR
721 # update PR
722 self.app.post(
722 self.app.post(
723 route_path('pullrequest_update',
723 route_path('pullrequest_update',
724 repo_name=target.repo_name,
724 repo_name=target.repo_name,
725 pull_request_id=pull_request_id),
725 pull_request_id=pull_request_id),
726 params={'update_commits': 'true',
726 params={'update_commits': 'true',
727 'csrf_token': csrf_token},
727 'csrf_token': csrf_token},
728 status=200)
728 status=200)
729
729
730 # check that we have now both revisions
730 # check that we have now both revisions
731 pull_request = PullRequest.get(pull_request_id)
731 pull_request = PullRequest.get(pull_request_id)
732 assert pull_request.revisions == [commit_ids['change-rebased']]
732 assert pull_request.revisions == [commit_ids['change-rebased']]
733 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
733 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
734 branch=backend.default_branch_name,
734 branch=backend.default_branch_name,
735 commit_id=commit_ids['ancestor-new'])
735 commit_id=commit_ids['ancestor-new'])
736
736
737 # TODO: johbo: This should be a test on its own
737 # TODO: johbo: This should be a test on its own
738 response = self.app.get(route_path(
738 response = self.app.get(route_path(
739 'pullrequest_new',
739 'pullrequest_new',
740 repo_name=target.repo_name))
740 repo_name=target.repo_name))
741 assert response.status_int == 200
741 assert response.status_int == 200
742 assert 'Pull request updated to' in response.body
742 assert 'Pull request updated to' in response.body
743 assert 'with 1 added, 1 removed commits.' in response.body
743 assert 'with 1 added, 1 removed commits.' in response.body
744
744
745 def test_update_of_ancestor_reference(self, backend, csrf_token):
745 def test_update_of_ancestor_reference(self, backend, csrf_token):
746 commits = [
746 commits = [
747 {'message': 'ancestor'},
747 {'message': 'ancestor'},
748 {'message': 'change'},
748 {'message': 'change'},
749 {'message': 'change-2'},
749 {'message': 'change-2'},
750 {'message': 'ancestor-new', 'parents': ['ancestor']},
750 {'message': 'ancestor-new', 'parents': ['ancestor']},
751 {'message': 'change-rebased'},
751 {'message': 'change-rebased'},
752 ]
752 ]
753 commit_ids = backend.create_master_repo(commits)
753 commit_ids = backend.create_master_repo(commits)
754 target = backend.create_repo(heads=['ancestor'])
754 target = backend.create_repo(heads=['ancestor'])
755 source = backend.create_repo(heads=['change'])
755 source = backend.create_repo(heads=['change'])
756
756
757 # create pr from a in source to A in target
757 # create pr from a in source to A in target
758 pull_request = PullRequest()
758 pull_request = PullRequest()
759 pull_request.source_repo = source
759 pull_request.source_repo = source
760 # TODO: johbo: Make sure that we write the source ref this way!
760 # TODO: johbo: Make sure that we write the source ref this way!
761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
762 branch=backend.default_branch_name,
762 branch=backend.default_branch_name,
763 commit_id=commit_ids['change'])
763 commit_id=commit_ids['change'])
764 pull_request.target_repo = target
764 pull_request.target_repo = target
765 # TODO: johbo: Target ref should be branch based, since tip can jump
765 # TODO: johbo: Target ref should be branch based, since tip can jump
766 # from branch to branch
766 # from branch to branch
767 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
767 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
768 branch=backend.default_branch_name,
768 branch=backend.default_branch_name,
769 commit_id=commit_ids['ancestor'])
769 commit_id=commit_ids['ancestor'])
770 pull_request.revisions = [commit_ids['change']]
770 pull_request.revisions = [commit_ids['change']]
771 pull_request.title = u"Test"
771 pull_request.title = u"Test"
772 pull_request.description = u"Description"
772 pull_request.description = u"Description"
773 pull_request.author = UserModel().get_by_username(
773 pull_request.author = UserModel().get_by_username(
774 TEST_USER_ADMIN_LOGIN)
774 TEST_USER_ADMIN_LOGIN)
775 Session().add(pull_request)
775 Session().add(pull_request)
776 Session().commit()
776 Session().commit()
777 pull_request_id = pull_request.pull_request_id
777 pull_request_id = pull_request.pull_request_id
778
778
779 # target has ancestor - ancestor-new
779 # target has ancestor - ancestor-new
780 # source has ancestor - ancestor-new - change-rebased
780 # source has ancestor - ancestor-new - change-rebased
781 backend.pull_heads(target, heads=['ancestor-new'])
781 backend.pull_heads(target, heads=['ancestor-new'])
782 backend.pull_heads(source, heads=['change-rebased'])
782 backend.pull_heads(source, heads=['change-rebased'])
783
783
784 # update PR
784 # update PR
785 self.app.post(
785 self.app.post(
786 route_path('pullrequest_update',
786 route_path('pullrequest_update',
787 repo_name=target.repo_name,
787 repo_name=target.repo_name,
788 pull_request_id=pull_request_id),
788 pull_request_id=pull_request_id),
789 params={'update_commits': 'true',
789 params={'update_commits': 'true',
790 'csrf_token': csrf_token},
790 'csrf_token': csrf_token},
791 status=200)
791 status=200)
792
792
793 # Expect the target reference to be updated correctly
793 # Expect the target reference to be updated correctly
794 pull_request = PullRequest.get(pull_request_id)
794 pull_request = PullRequest.get(pull_request_id)
795 assert pull_request.revisions == [commit_ids['change-rebased']]
795 assert pull_request.revisions == [commit_ids['change-rebased']]
796 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
796 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
797 branch=backend.default_branch_name,
797 branch=backend.default_branch_name,
798 commit_id=commit_ids['ancestor-new'])
798 commit_id=commit_ids['ancestor-new'])
799 assert pull_request.target_ref == expected_target_ref
799 assert pull_request.target_ref == expected_target_ref
800
800
801 def test_remove_pull_request_branch(self, backend_git, csrf_token):
801 def test_remove_pull_request_branch(self, backend_git, csrf_token):
802 branch_name = 'development'
802 branch_name = 'development'
803 commits = [
803 commits = [
804 {'message': 'initial-commit'},
804 {'message': 'initial-commit'},
805 {'message': 'old-feature'},
805 {'message': 'old-feature'},
806 {'message': 'new-feature', 'branch': branch_name},
806 {'message': 'new-feature', 'branch': branch_name},
807 ]
807 ]
808 repo = backend_git.create_repo(commits)
808 repo = backend_git.create_repo(commits)
809 commit_ids = backend_git.commit_ids
809 commit_ids = backend_git.commit_ids
810
810
811 pull_request = PullRequest()
811 pull_request = PullRequest()
812 pull_request.source_repo = repo
812 pull_request.source_repo = repo
813 pull_request.target_repo = repo
813 pull_request.target_repo = repo
814 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
814 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
815 branch=branch_name, commit_id=commit_ids['new-feature'])
815 branch=branch_name, commit_id=commit_ids['new-feature'])
816 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
816 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
817 branch=backend_git.default_branch_name,
817 branch=backend_git.default_branch_name,
818 commit_id=commit_ids['old-feature'])
818 commit_id=commit_ids['old-feature'])
819 pull_request.revisions = [commit_ids['new-feature']]
819 pull_request.revisions = [commit_ids['new-feature']]
820 pull_request.title = u"Test"
820 pull_request.title = u"Test"
821 pull_request.description = u"Description"
821 pull_request.description = u"Description"
822 pull_request.author = UserModel().get_by_username(
822 pull_request.author = UserModel().get_by_username(
823 TEST_USER_ADMIN_LOGIN)
823 TEST_USER_ADMIN_LOGIN)
824 Session().add(pull_request)
824 Session().add(pull_request)
825 Session().commit()
825 Session().commit()
826
826
827 vcs = repo.scm_instance()
827 vcs = repo.scm_instance()
828 vcs.remove_ref('refs/heads/{}'.format(branch_name))
828 vcs.remove_ref('refs/heads/{}'.format(branch_name))
829
829
830 response = self.app.get(route_path(
830 response = self.app.get(route_path(
831 'pullrequest_show',
831 'pullrequest_show',
832 repo_name=repo.repo_name,
832 repo_name=repo.repo_name,
833 pull_request_id=pull_request.pull_request_id))
833 pull_request_id=pull_request.pull_request_id))
834
834
835 assert response.status_int == 200
835 assert response.status_int == 200
836 assert_response = AssertResponse(response)
836 assert_response = AssertResponse(response)
837 assert_response.element_contains(
837 assert_response.element_contains(
838 '#changeset_compare_view_content .alert strong',
838 '#changeset_compare_view_content .alert strong',
839 'Missing commits')
839 'Missing commits')
840 assert_response.element_contains(
840 assert_response.element_contains(
841 '#changeset_compare_view_content .alert',
841 '#changeset_compare_view_content .alert',
842 'This pull request cannot be displayed, because one or more'
842 'This pull request cannot be displayed, because one or more'
843 ' commits no longer exist in the source repository.')
843 ' commits no longer exist in the source repository.')
844
844
845 def test_strip_commits_from_pull_request(
845 def test_strip_commits_from_pull_request(
846 self, backend, pr_util, csrf_token):
846 self, backend, pr_util, csrf_token):
847 commits = [
847 commits = [
848 {'message': 'initial-commit'},
848 {'message': 'initial-commit'},
849 {'message': 'old-feature'},
849 {'message': 'old-feature'},
850 {'message': 'new-feature', 'parents': ['initial-commit']},
850 {'message': 'new-feature', 'parents': ['initial-commit']},
851 ]
851 ]
852 pull_request = pr_util.create_pull_request(
852 pull_request = pr_util.create_pull_request(
853 commits, target_head='initial-commit', source_head='new-feature',
853 commits, target_head='initial-commit', source_head='new-feature',
854 revisions=['new-feature'])
854 revisions=['new-feature'])
855
855
856 vcs = pr_util.source_repository.scm_instance()
856 vcs = pr_util.source_repository.scm_instance()
857 if backend.alias == 'git':
857 if backend.alias == 'git':
858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
859 else:
859 else:
860 vcs.strip(pr_util.commit_ids['new-feature'])
860 vcs.strip(pr_util.commit_ids['new-feature'])
861
861
862 response = self.app.get(route_path(
862 response = self.app.get(route_path(
863 'pullrequest_show',
863 'pullrequest_show',
864 repo_name=pr_util.target_repository.repo_name,
864 repo_name=pr_util.target_repository.repo_name,
865 pull_request_id=pull_request.pull_request_id))
865 pull_request_id=pull_request.pull_request_id))
866
866
867 assert response.status_int == 200
867 assert response.status_int == 200
868 assert_response = AssertResponse(response)
868 assert_response = AssertResponse(response)
869 assert_response.element_contains(
869 assert_response.element_contains(
870 '#changeset_compare_view_content .alert strong',
870 '#changeset_compare_view_content .alert strong',
871 'Missing commits')
871 'Missing commits')
872 assert_response.element_contains(
872 assert_response.element_contains(
873 '#changeset_compare_view_content .alert',
873 '#changeset_compare_view_content .alert',
874 'This pull request cannot be displayed, because one or more'
874 'This pull request cannot be displayed, because one or more'
875 ' commits no longer exist in the source repository.')
875 ' commits no longer exist in the source repository.')
876 assert_response.element_contains(
876 assert_response.element_contains(
877 '#update_commits',
877 '#update_commits',
878 'Update commits')
878 'Update commits')
879
879
880 def test_strip_commits_and_update(
880 def test_strip_commits_and_update(
881 self, backend, pr_util, csrf_token):
881 self, backend, pr_util, csrf_token):
882 commits = [
882 commits = [
883 {'message': 'initial-commit'},
883 {'message': 'initial-commit'},
884 {'message': 'old-feature'},
884 {'message': 'old-feature'},
885 {'message': 'new-feature', 'parents': ['old-feature']},
885 {'message': 'new-feature', 'parents': ['old-feature']},
886 ]
886 ]
887 pull_request = pr_util.create_pull_request(
887 pull_request = pr_util.create_pull_request(
888 commits, target_head='old-feature', source_head='new-feature',
888 commits, target_head='old-feature', source_head='new-feature',
889 revisions=['new-feature'], mergeable=True)
889 revisions=['new-feature'], mergeable=True)
890
890
891 vcs = pr_util.source_repository.scm_instance()
891 vcs = pr_util.source_repository.scm_instance()
892 if backend.alias == 'git':
892 if backend.alias == 'git':
893 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
893 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
894 else:
894 else:
895 vcs.strip(pr_util.commit_ids['new-feature'])
895 vcs.strip(pr_util.commit_ids['new-feature'])
896
896
897 response = self.app.post(
897 response = self.app.post(
898 route_path('pullrequest_update',
898 route_path('pullrequest_update',
899 repo_name=pull_request.target_repo.repo_name,
899 repo_name=pull_request.target_repo.repo_name,
900 pull_request_id=pull_request.pull_request_id),
900 pull_request_id=pull_request.pull_request_id),
901 params={'update_commits': 'true',
901 params={'update_commits': 'true',
902 'csrf_token': csrf_token})
902 'csrf_token': csrf_token})
903
903
904 assert response.status_int == 200
904 assert response.status_int == 200
905 assert response.body == 'true'
905 assert response.body == 'true'
906
906
907 # Make sure that after update, it won't raise 500 errors
907 # Make sure that after update, it won't raise 500 errors
908 response = self.app.get(route_path(
908 response = self.app.get(route_path(
909 'pullrequest_show',
909 'pullrequest_show',
910 repo_name=pr_util.target_repository.repo_name,
910 repo_name=pr_util.target_repository.repo_name,
911 pull_request_id=pull_request.pull_request_id))
911 pull_request_id=pull_request.pull_request_id))
912
912
913 assert response.status_int == 200
913 assert response.status_int == 200
914 assert_response = AssertResponse(response)
914 assert_response = AssertResponse(response)
915 assert_response.element_contains(
915 assert_response.element_contains(
916 '#changeset_compare_view_content .alert strong',
916 '#changeset_compare_view_content .alert strong',
917 'Missing commits')
917 'Missing commits')
918
918
919 def test_branch_is_a_link(self, pr_util):
919 def test_branch_is_a_link(self, pr_util):
920 pull_request = pr_util.create_pull_request()
920 pull_request = pr_util.create_pull_request()
921 pull_request.source_ref = 'branch:origin:1234567890abcdef'
921 pull_request.source_ref = 'branch:origin:1234567890abcdef'
922 pull_request.target_ref = 'branch:target:abcdef1234567890'
922 pull_request.target_ref = 'branch:target:abcdef1234567890'
923 Session().add(pull_request)
923 Session().add(pull_request)
924 Session().commit()
924 Session().commit()
925
925
926 response = self.app.get(route_path(
926 response = self.app.get(route_path(
927 'pullrequest_show',
927 'pullrequest_show',
928 repo_name=pull_request.target_repo.scm_instance().name,
928 repo_name=pull_request.target_repo.scm_instance().name,
929 pull_request_id=pull_request.pull_request_id))
929 pull_request_id=pull_request.pull_request_id))
930 assert response.status_int == 200
930 assert response.status_int == 200
931 assert_response = AssertResponse(response)
931 assert_response = AssertResponse(response)
932
932
933 origin = assert_response.get_element('.pr-origininfo .tag')
933 origin = assert_response.get_element('.pr-origininfo .tag')
934 origin_children = origin.getchildren()
934 origin_children = origin.getchildren()
935 assert len(origin_children) == 1
935 assert len(origin_children) == 1
936 target = assert_response.get_element('.pr-targetinfo .tag')
936 target = assert_response.get_element('.pr-targetinfo .tag')
937 target_children = target.getchildren()
937 target_children = target.getchildren()
938 assert len(target_children) == 1
938 assert len(target_children) == 1
939
939
940 expected_origin_link = route_path(
940 expected_origin_link = route_path(
941 'repo_changelog',
941 'repo_changelog',
942 repo_name=pull_request.source_repo.scm_instance().name,
942 repo_name=pull_request.source_repo.scm_instance().name,
943 params=dict(branch='origin'))
943 params=dict(branch='origin'))
944 expected_target_link = route_path(
944 expected_target_link = route_path(
945 'repo_changelog',
945 'repo_changelog',
946 repo_name=pull_request.target_repo.scm_instance().name,
946 repo_name=pull_request.target_repo.scm_instance().name,
947 params=dict(branch='target'))
947 params=dict(branch='target'))
948 assert origin_children[0].attrib['href'] == expected_origin_link
948 assert origin_children[0].attrib['href'] == expected_origin_link
949 assert origin_children[0].text == 'branch: origin'
949 assert origin_children[0].text == 'branch: origin'
950 assert target_children[0].attrib['href'] == expected_target_link
950 assert target_children[0].attrib['href'] == expected_target_link
951 assert target_children[0].text == 'branch: target'
951 assert target_children[0].text == 'branch: target'
952
952
953 def test_bookmark_is_not_a_link(self, pr_util):
953 def test_bookmark_is_not_a_link(self, pr_util):
954 pull_request = pr_util.create_pull_request()
954 pull_request = pr_util.create_pull_request()
955 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
955 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
956 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
956 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
957 Session().add(pull_request)
957 Session().add(pull_request)
958 Session().commit()
958 Session().commit()
959
959
960 response = self.app.get(route_path(
960 response = self.app.get(route_path(
961 'pullrequest_show',
961 'pullrequest_show',
962 repo_name=pull_request.target_repo.scm_instance().name,
962 repo_name=pull_request.target_repo.scm_instance().name,
963 pull_request_id=pull_request.pull_request_id))
963 pull_request_id=pull_request.pull_request_id))
964 assert response.status_int == 200
964 assert response.status_int == 200
965 assert_response = AssertResponse(response)
965 assert_response = AssertResponse(response)
966
966
967 origin = assert_response.get_element('.pr-origininfo .tag')
967 origin = assert_response.get_element('.pr-origininfo .tag')
968 assert origin.text.strip() == 'bookmark: origin'
968 assert origin.text.strip() == 'bookmark: origin'
969 assert origin.getchildren() == []
969 assert origin.getchildren() == []
970
970
971 target = assert_response.get_element('.pr-targetinfo .tag')
971 target = assert_response.get_element('.pr-targetinfo .tag')
972 assert target.text.strip() == 'bookmark: target'
972 assert target.text.strip() == 'bookmark: target'
973 assert target.getchildren() == []
973 assert target.getchildren() == []
974
974
975 def test_tag_is_not_a_link(self, pr_util):
975 def test_tag_is_not_a_link(self, pr_util):
976 pull_request = pr_util.create_pull_request()
976 pull_request = pr_util.create_pull_request()
977 pull_request.source_ref = 'tag:origin:1234567890abcdef'
977 pull_request.source_ref = 'tag:origin:1234567890abcdef'
978 pull_request.target_ref = 'tag:target:abcdef1234567890'
978 pull_request.target_ref = 'tag:target:abcdef1234567890'
979 Session().add(pull_request)
979 Session().add(pull_request)
980 Session().commit()
980 Session().commit()
981
981
982 response = self.app.get(route_path(
982 response = self.app.get(route_path(
983 'pullrequest_show',
983 'pullrequest_show',
984 repo_name=pull_request.target_repo.scm_instance().name,
984 repo_name=pull_request.target_repo.scm_instance().name,
985 pull_request_id=pull_request.pull_request_id))
985 pull_request_id=pull_request.pull_request_id))
986 assert response.status_int == 200
986 assert response.status_int == 200
987 assert_response = AssertResponse(response)
987 assert_response = AssertResponse(response)
988
988
989 origin = assert_response.get_element('.pr-origininfo .tag')
989 origin = assert_response.get_element('.pr-origininfo .tag')
990 assert origin.text.strip() == 'tag: origin'
990 assert origin.text.strip() == 'tag: origin'
991 assert origin.getchildren() == []
991 assert origin.getchildren() == []
992
992
993 target = assert_response.get_element('.pr-targetinfo .tag')
993 target = assert_response.get_element('.pr-targetinfo .tag')
994 assert target.text.strip() == 'tag: target'
994 assert target.text.strip() == 'tag: target'
995 assert target.getchildren() == []
995 assert target.getchildren() == []
996
996
997 @pytest.mark.parametrize('mergeable', [True, False])
997 @pytest.mark.parametrize('mergeable', [True, False])
998 def test_shadow_repository_link(
998 def test_shadow_repository_link(
999 self, mergeable, pr_util, http_host_only_stub):
999 self, mergeable, pr_util, http_host_only_stub):
1000 """
1000 """
1001 Check that the pull request summary page displays a link to the shadow
1001 Check that the pull request summary page displays a link to the shadow
1002 repository if the pull request is mergeable. If it is not mergeable
1002 repository if the pull request is mergeable. If it is not mergeable
1003 the link should not be displayed.
1003 the link should not be displayed.
1004 """
1004 """
1005 pull_request = pr_util.create_pull_request(
1005 pull_request = pr_util.create_pull_request(
1006 mergeable=mergeable, enable_notifications=False)
1006 mergeable=mergeable, enable_notifications=False)
1007 target_repo = pull_request.target_repo.scm_instance()
1007 target_repo = pull_request.target_repo.scm_instance()
1008 pr_id = pull_request.pull_request_id
1008 pr_id = pull_request.pull_request_id
1009 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1009 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1010 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1010 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1011
1011
1012 response = self.app.get(route_path(
1012 response = self.app.get(route_path(
1013 'pullrequest_show',
1013 'pullrequest_show',
1014 repo_name=target_repo.name,
1014 repo_name=target_repo.name,
1015 pull_request_id=pr_id))
1015 pull_request_id=pr_id))
1016
1016
1017 assertr = AssertResponse(response)
1017 assertr = AssertResponse(response)
1018 if mergeable:
1018 if mergeable:
1019 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1019 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1020 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1020 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1021 else:
1021 else:
1022 assertr.no_element_exists('.pr-mergeinfo')
1022 assertr.no_element_exists('.pr-mergeinfo')
1023
1023
1024
1024
1025 @pytest.mark.usefixtures('app')
1025 @pytest.mark.usefixtures('app')
1026 @pytest.mark.backends("git", "hg")
1026 @pytest.mark.backends("git", "hg")
1027 class TestPullrequestsControllerDelete(object):
1027 class TestPullrequestsControllerDelete(object):
1028 def test_pull_request_delete_button_permissions_admin(
1028 def test_pull_request_delete_button_permissions_admin(
1029 self, autologin_user, user_admin, pr_util):
1029 self, autologin_user, user_admin, pr_util):
1030 pull_request = pr_util.create_pull_request(
1030 pull_request = pr_util.create_pull_request(
1031 author=user_admin.username, enable_notifications=False)
1031 author=user_admin.username, enable_notifications=False)
1032
1032
1033 response = self.app.get(route_path(
1033 response = self.app.get(route_path(
1034 'pullrequest_show',
1034 'pullrequest_show',
1035 repo_name=pull_request.target_repo.scm_instance().name,
1035 repo_name=pull_request.target_repo.scm_instance().name,
1036 pull_request_id=pull_request.pull_request_id))
1036 pull_request_id=pull_request.pull_request_id))
1037
1037
1038 response.mustcontain('id="delete_pullrequest"')
1038 response.mustcontain('id="delete_pullrequest"')
1039 response.mustcontain('Confirm to delete this pull request')
1039 response.mustcontain('Confirm to delete this pull request')
1040
1040
1041 def test_pull_request_delete_button_permissions_owner(
1041 def test_pull_request_delete_button_permissions_owner(
1042 self, autologin_regular_user, user_regular, pr_util):
1042 self, autologin_regular_user, user_regular, pr_util):
1043 pull_request = pr_util.create_pull_request(
1043 pull_request = pr_util.create_pull_request(
1044 author=user_regular.username, enable_notifications=False)
1044 author=user_regular.username, enable_notifications=False)
1045
1045
1046 response = self.app.get(route_path(
1046 response = self.app.get(route_path(
1047 'pullrequest_show',
1047 'pullrequest_show',
1048 repo_name=pull_request.target_repo.scm_instance().name,
1048 repo_name=pull_request.target_repo.scm_instance().name,
1049 pull_request_id=pull_request.pull_request_id))
1049 pull_request_id=pull_request.pull_request_id))
1050
1050
1051 response.mustcontain('id="delete_pullrequest"')
1051 response.mustcontain('id="delete_pullrequest"')
1052 response.mustcontain('Confirm to delete this pull request')
1052 response.mustcontain('Confirm to delete this pull request')
1053
1053
1054 def test_pull_request_delete_button_permissions_forbidden(
1054 def test_pull_request_delete_button_permissions_forbidden(
1055 self, autologin_regular_user, user_regular, user_admin, pr_util):
1055 self, autologin_regular_user, user_regular, user_admin, pr_util):
1056 pull_request = pr_util.create_pull_request(
1056 pull_request = pr_util.create_pull_request(
1057 author=user_admin.username, enable_notifications=False)
1057 author=user_admin.username, enable_notifications=False)
1058
1058
1059 response = self.app.get(route_path(
1059 response = self.app.get(route_path(
1060 'pullrequest_show',
1060 'pullrequest_show',
1061 repo_name=pull_request.target_repo.scm_instance().name,
1061 repo_name=pull_request.target_repo.scm_instance().name,
1062 pull_request_id=pull_request.pull_request_id))
1062 pull_request_id=pull_request.pull_request_id))
1063 response.mustcontain(no=['id="delete_pullrequest"'])
1063 response.mustcontain(no=['id="delete_pullrequest"'])
1064 response.mustcontain(no=['Confirm to delete this pull request'])
1064 response.mustcontain(no=['Confirm to delete this pull request'])
1065
1065
1066 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1066 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1067 self, autologin_regular_user, user_regular, user_admin, pr_util,
1067 self, autologin_regular_user, user_regular, user_admin, pr_util,
1068 user_util):
1068 user_util):
1069
1069
1070 pull_request = pr_util.create_pull_request(
1070 pull_request = pr_util.create_pull_request(
1071 author=user_admin.username, enable_notifications=False)
1071 author=user_admin.username, enable_notifications=False)
1072
1072
1073 user_util.grant_user_permission_to_repo(
1073 user_util.grant_user_permission_to_repo(
1074 pull_request.target_repo, user_regular,
1074 pull_request.target_repo, user_regular,
1075 'repository.write')
1075 'repository.write')
1076
1076
1077 response = self.app.get(route_path(
1077 response = self.app.get(route_path(
1078 'pullrequest_show',
1078 'pullrequest_show',
1079 repo_name=pull_request.target_repo.scm_instance().name,
1079 repo_name=pull_request.target_repo.scm_instance().name,
1080 pull_request_id=pull_request.pull_request_id))
1080 pull_request_id=pull_request.pull_request_id))
1081
1081
1082 response.mustcontain('id="open_edit_pullrequest"')
1082 response.mustcontain('id="open_edit_pullrequest"')
1083 response.mustcontain('id="delete_pullrequest"')
1083 response.mustcontain('id="delete_pullrequest"')
1084 response.mustcontain(no=['Confirm to delete this pull request'])
1084 response.mustcontain(no=['Confirm to delete this pull request'])
1085
1085
1086 def test_delete_comment_returns_404_if_comment_does_not_exist(
1086 def test_delete_comment_returns_404_if_comment_does_not_exist(
1087 self, autologin_user, pr_util, user_admin):
1087 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1088
1088
1089 pull_request = pr_util.create_pull_request(
1089 pull_request = pr_util.create_pull_request(
1090 author=user_admin.username, enable_notifications=False)
1090 author=user_admin.username, enable_notifications=False)
1091
1091
1092 self.app.get(route_path(
1092 self.app.post(
1093 route_path(
1093 'pullrequest_comment_delete',
1094 'pullrequest_comment_delete',
1094 repo_name=pull_request.target_repo.scm_instance().name,
1095 repo_name=pull_request.target_repo.scm_instance().name,
1095 pull_request_id=pull_request.pull_request_id,
1096 pull_request_id=pull_request.pull_request_id,
1096 comment_id=1024404), status=404)
1097 comment_id=1024404),
1098 extra_environ=xhr_header,
1099 params={'csrf_token': csrf_token},
1100 status=404
1101 )
1102
1103 def test_delete_comment(
1104 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1105
1106 pull_request = pr_util.create_pull_request(
1107 author=user_admin.username, enable_notifications=False)
1108 comment = pr_util.create_comment()
1109 comment_id = comment.comment_id
1110
1111 response = self.app.post(
1112 route_path(
1113 'pullrequest_comment_delete',
1114 repo_name=pull_request.target_repo.scm_instance().name,
1115 pull_request_id=pull_request.pull_request_id,
1116 comment_id=comment_id),
1117 extra_environ=xhr_header,
1118 params={'csrf_token': csrf_token},
1119 status=200
1120 )
1121 assert response.body == 'true'
1097
1122
1098
1123
1099 def assert_pull_request_status(pull_request, expected_status):
1124 def assert_pull_request_status(pull_request, expected_status):
1100 status = ChangesetStatusModel().calculated_review_status(
1125 status = ChangesetStatusModel().calculated_review_status(
1101 pull_request=pull_request)
1126 pull_request=pull_request)
1102 assert status == expected_status
1127 assert status == expected_status
1103
1128
1104
1129
1105 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1130 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1106 @pytest.mark.usefixtures("autologin_user")
1131 @pytest.mark.usefixtures("autologin_user")
1107 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1132 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1108 response = app.get(
1133 response = app.get(
1109 route_path(route, repo_name=backend_svn.repo_name), status=404)
1134 route_path(route, repo_name=backend_svn.repo_name), status=404)
1110
1135
@@ -1,1182 +1,1181 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import peppercorn
25 import peppercorn
26 from pyramid.httpexceptions import (
26 from pyramid.httpexceptions import (
27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 from pyramid.view import view_config
28 from pyramid.view import view_config
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30
30
31 from rhodecode import events
31 from rhodecode import events
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.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 from rhodecode.model.changeset_status import ChangesetStatusModel
43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.comment import CommentsModel
45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 ChangesetComment, ChangesetStatus, Repository)
46 ChangesetComment, ChangesetStatus, Repository)
47 from rhodecode.model.forms import PullRequestForm
47 from rhodecode.model.forms import PullRequestForm
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 from rhodecode.model.scm import ScmModel
50 from rhodecode.model.scm import ScmModel
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
56
56
57 def load_default_context(self):
57 def load_default_context(self):
58 c = self._get_local_tmpl_context(include_app_defaults=True)
58 c = self._get_local_tmpl_context(include_app_defaults=True)
59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
60 c.repo_info = self.db_repo
60 c.repo_info = self.db_repo
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 self._register_global_c(c)
63 self._register_global_c(c)
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'data_table/_dt_elements.mako')
72 'data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 '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 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176
176
177 # additional filters
177 # additional filters
178 req_get = self.request.GET
178 req_get = self.request.GET
179 source = str2bool(req_get.get('source'))
179 source = str2bool(req_get.get('source'))
180 closed = str2bool(req_get.get('closed'))
180 closed = str2bool(req_get.get('closed'))
181 my = str2bool(req_get.get('my'))
181 my = str2bool(req_get.get('my'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184
184
185 filter_type = 'awaiting_review' if awaiting_review \
185 filter_type = 'awaiting_review' if awaiting_review \
186 else 'awaiting_my_review' if awaiting_my_review \
186 else 'awaiting_my_review' if awaiting_my_review \
187 else None
187 else None
188
188
189 opened_by = None
189 opened_by = None
190 if my:
190 if my:
191 opened_by = [self._rhodecode_user.user_id]
191 opened_by = [self._rhodecode_user.user_id]
192
192
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 if closed:
194 if closed:
195 statuses = [PullRequest.STATUS_CLOSED]
195 statuses = [PullRequest.STATUS_CLOSED]
196
196
197 data = self._get_pull_requests_list(
197 data = self._get_pull_requests_list(
198 repo_name=self.db_repo_name, source=source,
198 repo_name=self.db_repo_name, source=source,
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200
200
201 return data
201 return data
202
202
203 def _get_pr_version(self, pull_request_id, version=None):
203 def _get_pr_version(self, pull_request_id, version=None):
204 at_version = None
204 at_version = None
205
205
206 if version and version == 'latest':
206 if version and version == 'latest':
207 pull_request_ver = PullRequest.get(pull_request_id)
207 pull_request_ver = PullRequest.get(pull_request_id)
208 pull_request_obj = pull_request_ver
208 pull_request_obj = pull_request_ver
209 _org_pull_request_obj = pull_request_obj
209 _org_pull_request_obj = pull_request_obj
210 at_version = 'latest'
210 at_version = 'latest'
211 elif version:
211 elif version:
212 pull_request_ver = PullRequestVersion.get_or_404(version)
212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 pull_request_obj = pull_request_ver
213 pull_request_obj = pull_request_ver
214 _org_pull_request_obj = pull_request_ver.pull_request
214 _org_pull_request_obj = pull_request_ver.pull_request
215 at_version = pull_request_ver.pull_request_version_id
215 at_version = pull_request_ver.pull_request_version_id
216 else:
216 else:
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 pull_request_id)
218 pull_request_id)
219
219
220 pull_request_display_obj = PullRequest.get_pr_display_object(
220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 pull_request_obj, _org_pull_request_obj)
221 pull_request_obj, _org_pull_request_obj)
222
222
223 return _org_pull_request_obj, pull_request_obj, \
223 return _org_pull_request_obj, pull_request_obj, \
224 pull_request_display_obj, at_version
224 pull_request_display_obj, at_version
225
225
226 def _get_diffset(self, source_repo_name, source_repo,
226 def _get_diffset(self, source_repo_name, source_repo,
227 source_ref_id, target_ref_id,
227 source_ref_id, target_ref_id,
228 target_commit, source_commit, diff_limit, fulldiff,
228 target_commit, source_commit, diff_limit, fulldiff,
229 file_limit, display_inline_comments):
229 file_limit, display_inline_comments):
230
230
231 vcs_diff = PullRequestModel().get_diff(
231 vcs_diff = PullRequestModel().get_diff(
232 source_repo, source_ref_id, target_ref_id)
232 source_repo, source_ref_id, target_ref_id)
233
233
234 diff_processor = diffs.DiffProcessor(
234 diff_processor = diffs.DiffProcessor(
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 file_limit=file_limit, show_full_diff=fulldiff)
236 file_limit=file_limit, show_full_diff=fulldiff)
237
237
238 _parsed = diff_processor.prepare()
238 _parsed = diff_processor.prepare()
239
239
240 def _node_getter(commit):
240 def _node_getter(commit):
241 def get_node(fname):
241 def get_node(fname):
242 try:
242 try:
243 return commit.get_node(fname)
243 return commit.get_node(fname)
244 except NodeDoesNotExistError:
244 except NodeDoesNotExistError:
245 return None
245 return None
246
246
247 return get_node
247 return get_node
248
248
249 diffset = codeblocks.DiffSet(
249 diffset = codeblocks.DiffSet(
250 repo_name=self.db_repo_name,
250 repo_name=self.db_repo_name,
251 source_repo_name=source_repo_name,
251 source_repo_name=source_repo_name,
252 source_node_getter=_node_getter(target_commit),
252 source_node_getter=_node_getter(target_commit),
253 target_node_getter=_node_getter(source_commit),
253 target_node_getter=_node_getter(source_commit),
254 comments=display_inline_comments
254 comments=display_inline_comments
255 )
255 )
256 diffset = diffset.render_patchset(
256 diffset = diffset.render_patchset(
257 _parsed, target_commit.raw_id, source_commit.raw_id)
257 _parsed, target_commit.raw_id, source_commit.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 pull_request_id = self.request.matchdict.get('pull_request_id')
268 pull_request_id = self.request.matchdict.get('pull_request_id')
269
269
270 c = self.load_default_context()
270 c = self.load_default_context()
271
271
272 version = self.request.GET.get('version')
272 version = self.request.GET.get('version')
273 from_version = self.request.GET.get('from_version') or version
273 from_version = self.request.GET.get('from_version') or version
274 merge_checks = self.request.GET.get('merge_checks')
274 merge_checks = self.request.GET.get('merge_checks')
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276
276
277 (pull_request_latest,
277 (pull_request_latest,
278 pull_request_at_ver,
278 pull_request_at_ver,
279 pull_request_display_obj,
279 pull_request_display_obj,
280 at_version) = self._get_pr_version(
280 at_version) = self._get_pr_version(
281 pull_request_id, version=version)
281 pull_request_id, version=version)
282 pr_closed = pull_request_latest.is_closed()
282 pr_closed = pull_request_latest.is_closed()
283
283
284 if pr_closed and (version or from_version):
284 if pr_closed and (version or from_version):
285 # not allow to browse versions
285 # not allow to browse versions
286 raise HTTPFound(h.route_path(
286 raise HTTPFound(h.route_path(
287 'pullrequest_show', repo_name=self.db_repo_name,
287 'pullrequest_show', repo_name=self.db_repo_name,
288 pull_request_id=pull_request_id))
288 pull_request_id=pull_request_id))
289
289
290 versions = pull_request_display_obj.versions()
290 versions = pull_request_display_obj.versions()
291
291
292 c.at_version = at_version
292 c.at_version = at_version
293 c.at_version_num = (at_version
293 c.at_version_num = (at_version
294 if at_version and at_version != 'latest'
294 if at_version and at_version != 'latest'
295 else None)
295 else None)
296 c.at_version_pos = ChangesetComment.get_index_from_version(
296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 c.at_version_num, versions)
297 c.at_version_num, versions)
298
298
299 (prev_pull_request_latest,
299 (prev_pull_request_latest,
300 prev_pull_request_at_ver,
300 prev_pull_request_at_ver,
301 prev_pull_request_display_obj,
301 prev_pull_request_display_obj,
302 prev_at_version) = self._get_pr_version(
302 prev_at_version) = self._get_pr_version(
303 pull_request_id, version=from_version)
303 pull_request_id, version=from_version)
304
304
305 c.from_version = prev_at_version
305 c.from_version = prev_at_version
306 c.from_version_num = (prev_at_version
306 c.from_version_num = (prev_at_version
307 if prev_at_version and prev_at_version != 'latest'
307 if prev_at_version and prev_at_version != 'latest'
308 else None)
308 else None)
309 c.from_version_pos = ChangesetComment.get_index_from_version(
309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 c.from_version_num, versions)
310 c.from_version_num, versions)
311
311
312 # define if we're in COMPARE mode or VIEW at version mode
312 # define if we're in COMPARE mode or VIEW at version mode
313 compare = at_version != prev_at_version
313 compare = at_version != prev_at_version
314
314
315 # pull_requests repo_name we opened it against
315 # pull_requests repo_name we opened it against
316 # ie. target_repo must match
316 # ie. target_repo must match
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 raise HTTPNotFound()
318 raise HTTPNotFound()
319
319
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 pull_request_at_ver)
321 pull_request_at_ver)
322
322
323 c.pull_request = pull_request_display_obj
323 c.pull_request = pull_request_display_obj
324 c.pull_request_latest = pull_request_latest
324 c.pull_request_latest = pull_request_latest
325
325
326 if compare or (at_version and not at_version == 'latest'):
326 if compare or (at_version and not at_version == 'latest'):
327 c.allowed_to_change_status = False
327 c.allowed_to_change_status = False
328 c.allowed_to_update = False
328 c.allowed_to_update = False
329 c.allowed_to_merge = False
329 c.allowed_to_merge = False
330 c.allowed_to_delete = False
330 c.allowed_to_delete = False
331 c.allowed_to_comment = False
331 c.allowed_to_comment = False
332 c.allowed_to_close = False
332 c.allowed_to_close = False
333 else:
333 else:
334 can_change_status = PullRequestModel().check_user_change_status(
334 can_change_status = PullRequestModel().check_user_change_status(
335 pull_request_at_ver, self._rhodecode_user)
335 pull_request_at_ver, self._rhodecode_user)
336 c.allowed_to_change_status = can_change_status and not pr_closed
336 c.allowed_to_change_status = can_change_status and not pr_closed
337
337
338 c.allowed_to_update = PullRequestModel().check_user_update(
338 c.allowed_to_update = PullRequestModel().check_user_update(
339 pull_request_latest, self._rhodecode_user) and not pr_closed
339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 pull_request_latest, self._rhodecode_user) and not pr_closed
341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 pull_request_latest, self._rhodecode_user) and not pr_closed
343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 c.allowed_to_comment = not pr_closed
344 c.allowed_to_comment = not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346
346
347 c.forbid_adding_reviewers = False
347 c.forbid_adding_reviewers = False
348 c.forbid_author_to_review = False
348 c.forbid_author_to_review = False
349 c.forbid_commit_author_to_review = False
349 c.forbid_commit_author_to_review = False
350
350
351 if pull_request_latest.reviewer_data and \
351 if pull_request_latest.reviewer_data and \
352 'rules' in pull_request_latest.reviewer_data:
352 'rules' in pull_request_latest.reviewer_data:
353 rules = pull_request_latest.reviewer_data['rules'] or {}
353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 try:
354 try:
355 c.forbid_adding_reviewers = rules.get(
355 c.forbid_adding_reviewers = rules.get(
356 'forbid_adding_reviewers')
356 'forbid_adding_reviewers')
357 c.forbid_author_to_review = rules.get(
357 c.forbid_author_to_review = rules.get(
358 'forbid_author_to_review')
358 'forbid_author_to_review')
359 c.forbid_commit_author_to_review = rules.get(
359 c.forbid_commit_author_to_review = rules.get(
360 'forbid_commit_author_to_review')
360 'forbid_commit_author_to_review')
361 except Exception:
361 except Exception:
362 pass
362 pass
363
363
364 # check merge capabilities
364 # check merge capabilities
365 _merge_check = MergeCheck.validate(
365 _merge_check = MergeCheck.validate(
366 pull_request_latest, user=self._rhodecode_user)
366 pull_request_latest, user=self._rhodecode_user)
367 c.pr_merge_errors = _merge_check.error_details
367 c.pr_merge_errors = _merge_check.error_details
368 c.pr_merge_possible = not _merge_check.failed
368 c.pr_merge_possible = not _merge_check.failed
369 c.pr_merge_message = _merge_check.merge_msg
369 c.pr_merge_message = _merge_check.merge_msg
370
370
371 c.pull_request_review_status = _merge_check.review_status
371 c.pull_request_review_status = _merge_check.review_status
372 if merge_checks:
372 if merge_checks:
373 self.request.override_renderer = \
373 self.request.override_renderer = \
374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
375 return self._get_template_context(c)
375 return self._get_template_context(c)
376
376
377 comments_model = CommentsModel()
377 comments_model = CommentsModel()
378
378
379 # reviewers and statuses
379 # reviewers and statuses
380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
382
382
383 # GENERAL COMMENTS with versions #
383 # GENERAL COMMENTS with versions #
384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
385 q = q.order_by(ChangesetComment.comment_id.asc())
385 q = q.order_by(ChangesetComment.comment_id.asc())
386 general_comments = q
386 general_comments = q
387
387
388 # pick comments we want to render at current version
388 # pick comments we want to render at current version
389 c.comment_versions = comments_model.aggregate_comments(
389 c.comment_versions = comments_model.aggregate_comments(
390 general_comments, versions, c.at_version_num)
390 general_comments, versions, c.at_version_num)
391 c.comments = c.comment_versions[c.at_version_num]['until']
391 c.comments = c.comment_versions[c.at_version_num]['until']
392
392
393 # INLINE COMMENTS with versions #
393 # INLINE COMMENTS with versions #
394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
395 q = q.order_by(ChangesetComment.comment_id.asc())
395 q = q.order_by(ChangesetComment.comment_id.asc())
396 inline_comments = q
396 inline_comments = q
397
397
398 c.inline_versions = comments_model.aggregate_comments(
398 c.inline_versions = comments_model.aggregate_comments(
399 inline_comments, versions, c.at_version_num, inline=True)
399 inline_comments, versions, c.at_version_num, inline=True)
400
400
401 # inject latest version
401 # inject latest version
402 latest_ver = PullRequest.get_pr_display_object(
402 latest_ver = PullRequest.get_pr_display_object(
403 pull_request_latest, pull_request_latest)
403 pull_request_latest, pull_request_latest)
404
404
405 c.versions = versions + [latest_ver]
405 c.versions = versions + [latest_ver]
406
406
407 # if we use version, then do not show later comments
407 # if we use version, then do not show later comments
408 # than current version
408 # than current version
409 display_inline_comments = collections.defaultdict(
409 display_inline_comments = collections.defaultdict(
410 lambda: collections.defaultdict(list))
410 lambda: collections.defaultdict(list))
411 for co in inline_comments:
411 for co in inline_comments:
412 if c.at_version_num:
412 if c.at_version_num:
413 # pick comments that are at least UPTO given version, so we
413 # pick comments that are at least UPTO given version, so we
414 # don't render comments for higher version
414 # don't render comments for higher version
415 should_render = co.pull_request_version_id and \
415 should_render = co.pull_request_version_id and \
416 co.pull_request_version_id <= c.at_version_num
416 co.pull_request_version_id <= c.at_version_num
417 else:
417 else:
418 # showing all, for 'latest'
418 # showing all, for 'latest'
419 should_render = True
419 should_render = True
420
420
421 if should_render:
421 if should_render:
422 display_inline_comments[co.f_path][co.line_no].append(co)
422 display_inline_comments[co.f_path][co.line_no].append(co)
423
423
424 # load diff data into template context, if we use compare mode then
424 # load diff data into template context, if we use compare mode then
425 # diff is calculated based on changes between versions of PR
425 # diff is calculated based on changes between versions of PR
426
426
427 source_repo = pull_request_at_ver.source_repo
427 source_repo = pull_request_at_ver.source_repo
428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
429
429
430 target_repo = pull_request_at_ver.target_repo
430 target_repo = pull_request_at_ver.target_repo
431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
432
432
433 if compare:
433 if compare:
434 # in compare switch the diff base to latest commit from prev version
434 # in compare switch the diff base to latest commit from prev version
435 target_ref_id = prev_pull_request_display_obj.revisions[0]
435 target_ref_id = prev_pull_request_display_obj.revisions[0]
436
436
437 # despite opening commits for bookmarks/branches/tags, we always
437 # despite opening commits for bookmarks/branches/tags, we always
438 # convert this to rev to prevent changes after bookmark or branch change
438 # convert this to rev to prevent changes after bookmark or branch change
439 c.source_ref_type = 'rev'
439 c.source_ref_type = 'rev'
440 c.source_ref = source_ref_id
440 c.source_ref = source_ref_id
441
441
442 c.target_ref_type = 'rev'
442 c.target_ref_type = 'rev'
443 c.target_ref = target_ref_id
443 c.target_ref = target_ref_id
444
444
445 c.source_repo = source_repo
445 c.source_repo = source_repo
446 c.target_repo = target_repo
446 c.target_repo = target_repo
447
447
448 c.commit_ranges = []
448 c.commit_ranges = []
449 source_commit = EmptyCommit()
449 source_commit = EmptyCommit()
450 target_commit = EmptyCommit()
450 target_commit = EmptyCommit()
451 c.missing_requirements = False
451 c.missing_requirements = False
452
452
453 source_scm = source_repo.scm_instance()
453 source_scm = source_repo.scm_instance()
454 target_scm = target_repo.scm_instance()
454 target_scm = target_repo.scm_instance()
455
455
456 # try first shadow repo, fallback to regular repo
456 # try first shadow repo, fallback to regular repo
457 try:
457 try:
458 commits_source_repo = pull_request_latest.get_shadow_repo()
458 commits_source_repo = pull_request_latest.get_shadow_repo()
459 except Exception:
459 except Exception:
460 log.debug('Failed to get shadow repo', exc_info=True)
460 log.debug('Failed to get shadow repo', exc_info=True)
461 commits_source_repo = source_scm
461 commits_source_repo = source_scm
462
462
463 c.commits_source_repo = commits_source_repo
463 c.commits_source_repo = commits_source_repo
464 commit_cache = {}
464 commit_cache = {}
465 try:
465 try:
466 pre_load = ["author", "branch", "date", "message"]
466 pre_load = ["author", "branch", "date", "message"]
467 show_revs = pull_request_at_ver.revisions
467 show_revs = pull_request_at_ver.revisions
468 for rev in show_revs:
468 for rev in show_revs:
469 comm = commits_source_repo.get_commit(
469 comm = commits_source_repo.get_commit(
470 commit_id=rev, pre_load=pre_load)
470 commit_id=rev, pre_load=pre_load)
471 c.commit_ranges.append(comm)
471 c.commit_ranges.append(comm)
472 commit_cache[comm.raw_id] = comm
472 commit_cache[comm.raw_id] = comm
473
473
474 # Order here matters, we first need to get target, and then
474 # Order here matters, we first need to get target, and then
475 # the source
475 # the source
476 target_commit = commits_source_repo.get_commit(
476 target_commit = commits_source_repo.get_commit(
477 commit_id=safe_str(target_ref_id))
477 commit_id=safe_str(target_ref_id))
478
478
479 source_commit = commits_source_repo.get_commit(
479 source_commit = commits_source_repo.get_commit(
480 commit_id=safe_str(source_ref_id))
480 commit_id=safe_str(source_ref_id))
481
481
482 except CommitDoesNotExistError:
482 except CommitDoesNotExistError:
483 log.warning(
483 log.warning(
484 'Failed to get commit from `{}` repo'.format(
484 'Failed to get commit from `{}` repo'.format(
485 commits_source_repo), exc_info=True)
485 commits_source_repo), exc_info=True)
486 except RepositoryRequirementError:
486 except RepositoryRequirementError:
487 log.warning(
487 log.warning(
488 'Failed to get all required data from repo', exc_info=True)
488 'Failed to get all required data from repo', exc_info=True)
489 c.missing_requirements = True
489 c.missing_requirements = True
490
490
491 c.ancestor = None # set it to None, to hide it from PR view
491 c.ancestor = None # set it to None, to hide it from PR view
492
492
493 try:
493 try:
494 ancestor_id = source_scm.get_common_ancestor(
494 ancestor_id = source_scm.get_common_ancestor(
495 source_commit.raw_id, target_commit.raw_id, target_scm)
495 source_commit.raw_id, target_commit.raw_id, target_scm)
496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
497 except Exception:
497 except Exception:
498 c.ancestor_commit = None
498 c.ancestor_commit = None
499
499
500 c.statuses = source_repo.statuses(
500 c.statuses = source_repo.statuses(
501 [x.raw_id for x in c.commit_ranges])
501 [x.raw_id for x in c.commit_ranges])
502
502
503 # auto collapse if we have more than limit
503 # auto collapse if we have more than limit
504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
506 c.compare_mode = compare
506 c.compare_mode = compare
507
507
508 # diff_limit is the old behavior, will cut off the whole diff
508 # diff_limit is the old behavior, will cut off the whole diff
509 # if the limit is applied otherwise will just hide the
509 # if the limit is applied otherwise will just hide the
510 # big files from the front-end
510 # big files from the front-end
511 diff_limit = c.visual.cut_off_limit_diff
511 diff_limit = c.visual.cut_off_limit_diff
512 file_limit = c.visual.cut_off_limit_file
512 file_limit = c.visual.cut_off_limit_file
513
513
514 c.missing_commits = False
514 c.missing_commits = False
515 if (c.missing_requirements
515 if (c.missing_requirements
516 or isinstance(source_commit, EmptyCommit)
516 or isinstance(source_commit, EmptyCommit)
517 or source_commit == target_commit):
517 or source_commit == target_commit):
518
518
519 c.missing_commits = True
519 c.missing_commits = True
520 else:
520 else:
521
521
522 c.diffset = self._get_diffset(
522 c.diffset = self._get_diffset(
523 c.source_repo.repo_name, commits_source_repo,
523 c.source_repo.repo_name, commits_source_repo,
524 source_ref_id, target_ref_id,
524 source_ref_id, target_ref_id,
525 target_commit, source_commit,
525 target_commit, source_commit,
526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
527
527
528 c.limited_diff = c.diffset.limited_diff
528 c.limited_diff = c.diffset.limited_diff
529
529
530 # calculate removed files that are bound to comments
530 # calculate removed files that are bound to comments
531 comment_deleted_files = [
531 comment_deleted_files = [
532 fname for fname in display_inline_comments
532 fname for fname in display_inline_comments
533 if fname not in c.diffset.file_stats]
533 if fname not in c.diffset.file_stats]
534
534
535 c.deleted_files_comments = collections.defaultdict(dict)
535 c.deleted_files_comments = collections.defaultdict(dict)
536 for fname, per_line_comments in display_inline_comments.items():
536 for fname, per_line_comments in display_inline_comments.items():
537 if fname in comment_deleted_files:
537 if fname in comment_deleted_files:
538 c.deleted_files_comments[fname]['stats'] = 0
538 c.deleted_files_comments[fname]['stats'] = 0
539 c.deleted_files_comments[fname]['comments'] = list()
539 c.deleted_files_comments[fname]['comments'] = list()
540 for lno, comments in per_line_comments.items():
540 for lno, comments in per_line_comments.items():
541 c.deleted_files_comments[fname]['comments'].extend(
541 c.deleted_files_comments[fname]['comments'].extend(
542 comments)
542 comments)
543
543
544 # this is a hack to properly display links, when creating PR, the
544 # this is a hack to properly display links, when creating PR, the
545 # compare view and others uses different notation, and
545 # compare view and others uses different notation, and
546 # compare_commits.mako renders links based on the target_repo.
546 # compare_commits.mako renders links based on the target_repo.
547 # We need to swap that here to generate it properly on the html side
547 # We need to swap that here to generate it properly on the html side
548 c.target_repo = c.source_repo
548 c.target_repo = c.source_repo
549
549
550 c.commit_statuses = ChangesetStatus.STATUSES
550 c.commit_statuses = ChangesetStatus.STATUSES
551
551
552 c.show_version_changes = not pr_closed
552 c.show_version_changes = not pr_closed
553 if c.show_version_changes:
553 if c.show_version_changes:
554 cur_obj = pull_request_at_ver
554 cur_obj = pull_request_at_ver
555 prev_obj = prev_pull_request_at_ver
555 prev_obj = prev_pull_request_at_ver
556
556
557 old_commit_ids = prev_obj.revisions
557 old_commit_ids = prev_obj.revisions
558 new_commit_ids = cur_obj.revisions
558 new_commit_ids = cur_obj.revisions
559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
560 old_commit_ids, new_commit_ids)
560 old_commit_ids, new_commit_ids)
561 c.commit_changes_summary = commit_changes
561 c.commit_changes_summary = commit_changes
562
562
563 # calculate the diff for commits between versions
563 # calculate the diff for commits between versions
564 c.commit_changes = []
564 c.commit_changes = []
565 mark = lambda cs, fw: list(
565 mark = lambda cs, fw: list(
566 h.itertools.izip_longest([], cs, fillvalue=fw))
566 h.itertools.izip_longest([], cs, fillvalue=fw))
567 for c_type, raw_id in mark(commit_changes.added, 'a') \
567 for c_type, raw_id in mark(commit_changes.added, 'a') \
568 + mark(commit_changes.removed, 'r') \
568 + mark(commit_changes.removed, 'r') \
569 + mark(commit_changes.common, 'c'):
569 + mark(commit_changes.common, 'c'):
570
570
571 if raw_id in commit_cache:
571 if raw_id in commit_cache:
572 commit = commit_cache[raw_id]
572 commit = commit_cache[raw_id]
573 else:
573 else:
574 try:
574 try:
575 commit = commits_source_repo.get_commit(raw_id)
575 commit = commits_source_repo.get_commit(raw_id)
576 except CommitDoesNotExistError:
576 except CommitDoesNotExistError:
577 # in case we fail extracting still use "dummy" commit
577 # in case we fail extracting still use "dummy" commit
578 # for display in commit diff
578 # for display in commit diff
579 commit = h.AttributeDict(
579 commit = h.AttributeDict(
580 {'raw_id': raw_id,
580 {'raw_id': raw_id,
581 'message': 'EMPTY or MISSING COMMIT'})
581 'message': 'EMPTY or MISSING COMMIT'})
582 c.commit_changes.append([c_type, commit])
582 c.commit_changes.append([c_type, commit])
583
583
584 # current user review statuses for each version
584 # current user review statuses for each version
585 c.review_versions = {}
585 c.review_versions = {}
586 if self._rhodecode_user.user_id in allowed_reviewers:
586 if self._rhodecode_user.user_id in allowed_reviewers:
587 for co in general_comments:
587 for co in general_comments:
588 if co.author.user_id == self._rhodecode_user.user_id:
588 if co.author.user_id == self._rhodecode_user.user_id:
589 # each comment has a status change
589 # each comment has a status change
590 status = co.status_change
590 status = co.status_change
591 if status:
591 if status:
592 _ver_pr = status[0].comment.pull_request_version_id
592 _ver_pr = status[0].comment.pull_request_version_id
593 c.review_versions[_ver_pr] = status[0]
593 c.review_versions[_ver_pr] = status[0]
594
594
595 return self._get_template_context(c)
595 return self._get_template_context(c)
596
596
597 def assure_not_empty_repo(self):
597 def assure_not_empty_repo(self):
598 _ = self.request.translate
598 _ = self.request.translate
599
599
600 try:
600 try:
601 self.db_repo.scm_instance().get_commit()
601 self.db_repo.scm_instance().get_commit()
602 except EmptyRepositoryError:
602 except EmptyRepositoryError:
603 h.flash(h.literal(_('There are no commits yet')),
603 h.flash(h.literal(_('There are no commits yet')),
604 category='warning')
604 category='warning')
605 raise HTTPFound(
605 raise HTTPFound(
606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
607
607
608 @LoginRequired()
608 @LoginRequired()
609 @NotAnonymous()
609 @NotAnonymous()
610 @HasRepoPermissionAnyDecorator(
610 @HasRepoPermissionAnyDecorator(
611 'repository.read', 'repository.write', 'repository.admin')
611 'repository.read', 'repository.write', 'repository.admin')
612 @view_config(
612 @view_config(
613 route_name='pullrequest_new', request_method='GET',
613 route_name='pullrequest_new', request_method='GET',
614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
615 def pull_request_new(self):
615 def pull_request_new(self):
616 _ = self.request.translate
616 _ = self.request.translate
617 c = self.load_default_context()
617 c = self.load_default_context()
618
618
619 self.assure_not_empty_repo()
619 self.assure_not_empty_repo()
620 source_repo = self.db_repo
620 source_repo = self.db_repo
621
621
622 commit_id = self.request.GET.get('commit')
622 commit_id = self.request.GET.get('commit')
623 branch_ref = self.request.GET.get('branch')
623 branch_ref = self.request.GET.get('branch')
624 bookmark_ref = self.request.GET.get('bookmark')
624 bookmark_ref = self.request.GET.get('bookmark')
625
625
626 try:
626 try:
627 source_repo_data = PullRequestModel().generate_repo_data(
627 source_repo_data = PullRequestModel().generate_repo_data(
628 source_repo, commit_id=commit_id,
628 source_repo, commit_id=commit_id,
629 branch=branch_ref, bookmark=bookmark_ref)
629 branch=branch_ref, bookmark=bookmark_ref)
630 except CommitDoesNotExistError as e:
630 except CommitDoesNotExistError as e:
631 log.exception(e)
631 log.exception(e)
632 h.flash(_('Commit does not exist'), 'error')
632 h.flash(_('Commit does not exist'), 'error')
633 raise HTTPFound(
633 raise HTTPFound(
634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
635
635
636 default_target_repo = source_repo
636 default_target_repo = source_repo
637
637
638 if source_repo.parent:
638 if source_repo.parent:
639 parent_vcs_obj = source_repo.parent.scm_instance()
639 parent_vcs_obj = source_repo.parent.scm_instance()
640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
641 # change default if we have a parent repo
641 # change default if we have a parent repo
642 default_target_repo = source_repo.parent
642 default_target_repo = source_repo.parent
643
643
644 target_repo_data = PullRequestModel().generate_repo_data(
644 target_repo_data = PullRequestModel().generate_repo_data(
645 default_target_repo)
645 default_target_repo)
646
646
647 selected_source_ref = source_repo_data['refs']['selected_ref']
647 selected_source_ref = source_repo_data['refs']['selected_ref']
648
648
649 title_source_ref = selected_source_ref.split(':', 2)[1]
649 title_source_ref = selected_source_ref.split(':', 2)[1]
650 c.default_title = PullRequestModel().generate_pullrequest_title(
650 c.default_title = PullRequestModel().generate_pullrequest_title(
651 source=source_repo.repo_name,
651 source=source_repo.repo_name,
652 source_ref=title_source_ref,
652 source_ref=title_source_ref,
653 target=default_target_repo.repo_name
653 target=default_target_repo.repo_name
654 )
654 )
655
655
656 c.default_repo_data = {
656 c.default_repo_data = {
657 'source_repo_name': source_repo.repo_name,
657 'source_repo_name': source_repo.repo_name,
658 'source_refs_json': json.dumps(source_repo_data),
658 'source_refs_json': json.dumps(source_repo_data),
659 'target_repo_name': default_target_repo.repo_name,
659 'target_repo_name': default_target_repo.repo_name,
660 'target_refs_json': json.dumps(target_repo_data),
660 'target_refs_json': json.dumps(target_repo_data),
661 }
661 }
662 c.default_source_ref = selected_source_ref
662 c.default_source_ref = selected_source_ref
663
663
664 return self._get_template_context(c)
664 return self._get_template_context(c)
665
665
666 @LoginRequired()
666 @LoginRequired()
667 @NotAnonymous()
667 @NotAnonymous()
668 @HasRepoPermissionAnyDecorator(
668 @HasRepoPermissionAnyDecorator(
669 'repository.read', 'repository.write', 'repository.admin')
669 'repository.read', 'repository.write', 'repository.admin')
670 @view_config(
670 @view_config(
671 route_name='pullrequest_repo_refs', request_method='GET',
671 route_name='pullrequest_repo_refs', request_method='GET',
672 renderer='json_ext', xhr=True)
672 renderer='json_ext', xhr=True)
673 def pull_request_repo_refs(self):
673 def pull_request_repo_refs(self):
674 target_repo_name = self.request.matchdict['target_repo_name']
674 target_repo_name = self.request.matchdict['target_repo_name']
675 repo = Repository.get_by_repo_name(target_repo_name)
675 repo = Repository.get_by_repo_name(target_repo_name)
676 if not repo:
676 if not repo:
677 raise HTTPNotFound()
677 raise HTTPNotFound()
678 return PullRequestModel().generate_repo_data(repo)
678 return PullRequestModel().generate_repo_data(repo)
679
679
680 @LoginRequired()
680 @LoginRequired()
681 @NotAnonymous()
681 @NotAnonymous()
682 @HasRepoPermissionAnyDecorator(
682 @HasRepoPermissionAnyDecorator(
683 'repository.read', 'repository.write', 'repository.admin')
683 'repository.read', 'repository.write', 'repository.admin')
684 @view_config(
684 @view_config(
685 route_name='pullrequest_repo_destinations', request_method='GET',
685 route_name='pullrequest_repo_destinations', request_method='GET',
686 renderer='json_ext', xhr=True)
686 renderer='json_ext', xhr=True)
687 def pull_request_repo_destinations(self):
687 def pull_request_repo_destinations(self):
688 _ = self.request.translate
688 _ = self.request.translate
689 filter_query = self.request.GET.get('query')
689 filter_query = self.request.GET.get('query')
690
690
691 query = Repository.query() \
691 query = Repository.query() \
692 .order_by(func.length(Repository.repo_name)) \
692 .order_by(func.length(Repository.repo_name)) \
693 .filter(
693 .filter(
694 or_(Repository.repo_name == self.db_repo.repo_name,
694 or_(Repository.repo_name == self.db_repo.repo_name,
695 Repository.fork_id == self.db_repo.repo_id))
695 Repository.fork_id == self.db_repo.repo_id))
696
696
697 if filter_query:
697 if filter_query:
698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
699 query = query.filter(
699 query = query.filter(
700 Repository.repo_name.ilike(ilike_expression))
700 Repository.repo_name.ilike(ilike_expression))
701
701
702 add_parent = False
702 add_parent = False
703 if self.db_repo.parent:
703 if self.db_repo.parent:
704 if filter_query in self.db_repo.parent.repo_name:
704 if filter_query in self.db_repo.parent.repo_name:
705 parent_vcs_obj = self.db_repo.parent.scm_instance()
705 parent_vcs_obj = self.db_repo.parent.scm_instance()
706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
707 add_parent = True
707 add_parent = True
708
708
709 limit = 20 - 1 if add_parent else 20
709 limit = 20 - 1 if add_parent else 20
710 all_repos = query.limit(limit).all()
710 all_repos = query.limit(limit).all()
711 if add_parent:
711 if add_parent:
712 all_repos += [self.db_repo.parent]
712 all_repos += [self.db_repo.parent]
713
713
714 repos = []
714 repos = []
715 for obj in ScmModel().get_repos(all_repos):
715 for obj in ScmModel().get_repos(all_repos):
716 repos.append({
716 repos.append({
717 'id': obj['name'],
717 'id': obj['name'],
718 'text': obj['name'],
718 'text': obj['name'],
719 'type': 'repo',
719 'type': 'repo',
720 'obj': obj['dbrepo']
720 'obj': obj['dbrepo']
721 })
721 })
722
722
723 data = {
723 data = {
724 'more': False,
724 'more': False,
725 'results': [{
725 'results': [{
726 'text': _('Repositories'),
726 'text': _('Repositories'),
727 'children': repos
727 'children': repos
728 }] if repos else []
728 }] if repos else []
729 }
729 }
730 return data
730 return data
731
731
732 @LoginRequired()
732 @LoginRequired()
733 @NotAnonymous()
733 @NotAnonymous()
734 @HasRepoPermissionAnyDecorator(
734 @HasRepoPermissionAnyDecorator(
735 'repository.read', 'repository.write', 'repository.admin')
735 'repository.read', 'repository.write', 'repository.admin')
736 @CSRFRequired()
736 @CSRFRequired()
737 @view_config(
737 @view_config(
738 route_name='pullrequest_create', request_method='POST',
738 route_name='pullrequest_create', request_method='POST',
739 renderer=None)
739 renderer=None)
740 def pull_request_create(self):
740 def pull_request_create(self):
741 _ = self.request.translate
741 _ = self.request.translate
742 self.assure_not_empty_repo()
742 self.assure_not_empty_repo()
743
743
744 controls = peppercorn.parse(self.request.POST.items())
744 controls = peppercorn.parse(self.request.POST.items())
745
745
746 try:
746 try:
747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
748 except formencode.Invalid as errors:
748 except formencode.Invalid as errors:
749 if errors.error_dict.get('revisions'):
749 if errors.error_dict.get('revisions'):
750 msg = 'Revisions: %s' % errors.error_dict['revisions']
750 msg = 'Revisions: %s' % errors.error_dict['revisions']
751 elif errors.error_dict.get('pullrequest_title'):
751 elif errors.error_dict.get('pullrequest_title'):
752 msg = _('Pull request requires a title with min. 3 chars')
752 msg = _('Pull request requires a title with min. 3 chars')
753 else:
753 else:
754 msg = _('Error creating pull request: {}').format(errors)
754 msg = _('Error creating pull request: {}').format(errors)
755 log.exception(msg)
755 log.exception(msg)
756 h.flash(msg, 'error')
756 h.flash(msg, 'error')
757
757
758 # would rather just go back to form ...
758 # would rather just go back to form ...
759 raise HTTPFound(
759 raise HTTPFound(
760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
761
761
762 source_repo = _form['source_repo']
762 source_repo = _form['source_repo']
763 source_ref = _form['source_ref']
763 source_ref = _form['source_ref']
764 target_repo = _form['target_repo']
764 target_repo = _form['target_repo']
765 target_ref = _form['target_ref']
765 target_ref = _form['target_ref']
766 commit_ids = _form['revisions'][::-1]
766 commit_ids = _form['revisions'][::-1]
767
767
768 # find the ancestor for this pr
768 # find the ancestor for this pr
769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
771
771
772 source_scm = source_db_repo.scm_instance()
772 source_scm = source_db_repo.scm_instance()
773 target_scm = target_db_repo.scm_instance()
773 target_scm = target_db_repo.scm_instance()
774
774
775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
777
777
778 ancestor = source_scm.get_common_ancestor(
778 ancestor = source_scm.get_common_ancestor(
779 source_commit.raw_id, target_commit.raw_id, target_scm)
779 source_commit.raw_id, target_commit.raw_id, target_scm)
780
780
781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
783
783
784 pullrequest_title = _form['pullrequest_title']
784 pullrequest_title = _form['pullrequest_title']
785 title_source_ref = source_ref.split(':', 2)[1]
785 title_source_ref = source_ref.split(':', 2)[1]
786 if not pullrequest_title:
786 if not pullrequest_title:
787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
788 source=source_repo,
788 source=source_repo,
789 source_ref=title_source_ref,
789 source_ref=title_source_ref,
790 target=target_repo
790 target=target_repo
791 )
791 )
792
792
793 description = _form['pullrequest_desc']
793 description = _form['pullrequest_desc']
794
794
795 get_default_reviewers_data, validate_default_reviewers = \
795 get_default_reviewers_data, validate_default_reviewers = \
796 PullRequestModel().get_reviewer_functions()
796 PullRequestModel().get_reviewer_functions()
797
797
798 # recalculate reviewers logic, to make sure we can validate this
798 # recalculate reviewers logic, to make sure we can validate this
799 reviewer_rules = get_default_reviewers_data(
799 reviewer_rules = get_default_reviewers_data(
800 self._rhodecode_db_user, source_db_repo,
800 self._rhodecode_db_user, source_db_repo,
801 source_commit, target_db_repo, target_commit)
801 source_commit, target_db_repo, target_commit)
802
802
803 given_reviewers = _form['review_members']
803 given_reviewers = _form['review_members']
804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
805
805
806 try:
806 try:
807 pull_request = PullRequestModel().create(
807 pull_request = PullRequestModel().create(
808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
809 target_ref, commit_ids, reviewers, pullrequest_title,
809 target_ref, commit_ids, reviewers, pullrequest_title,
810 description, reviewer_rules
810 description, reviewer_rules
811 )
811 )
812 Session().commit()
812 Session().commit()
813 h.flash(_('Successfully opened new pull request'),
813 h.flash(_('Successfully opened new pull request'),
814 category='success')
814 category='success')
815 except Exception as e:
815 except Exception as e:
816 msg = _('Error occurred during creation of this pull request.')
816 msg = _('Error occurred during creation of this pull request.')
817 log.exception(msg)
817 log.exception(msg)
818 h.flash(msg, category='error')
818 h.flash(msg, category='error')
819 raise HTTPFound(
819 raise HTTPFound(
820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
821
821
822 raise HTTPFound(
822 raise HTTPFound(
823 h.route_path('pullrequest_show', repo_name=target_repo,
823 h.route_path('pullrequest_show', repo_name=target_repo,
824 pull_request_id=pull_request.pull_request_id))
824 pull_request_id=pull_request.pull_request_id))
825
825
826 @LoginRequired()
826 @LoginRequired()
827 @NotAnonymous()
827 @NotAnonymous()
828 @HasRepoPermissionAnyDecorator(
828 @HasRepoPermissionAnyDecorator(
829 'repository.read', 'repository.write', 'repository.admin')
829 'repository.read', 'repository.write', 'repository.admin')
830 @CSRFRequired()
830 @CSRFRequired()
831 @view_config(
831 @view_config(
832 route_name='pullrequest_update', request_method='POST',
832 route_name='pullrequest_update', request_method='POST',
833 renderer='json_ext')
833 renderer='json_ext')
834 def pull_request_update(self):
834 def pull_request_update(self):
835 pull_request_id = self.request.matchdict['pull_request_id']
835 pull_request_id = self.request.matchdict['pull_request_id']
836 pull_request = PullRequest.get_or_404(pull_request_id)
836 pull_request = PullRequest.get_or_404(pull_request_id)
837
837
838 # only owner or admin can update it
838 # only owner or admin can update it
839 allowed_to_update = PullRequestModel().check_user_update(
839 allowed_to_update = PullRequestModel().check_user_update(
840 pull_request, self._rhodecode_user)
840 pull_request, self._rhodecode_user)
841 if allowed_to_update:
841 if allowed_to_update:
842 controls = peppercorn.parse(self.request.POST.items())
842 controls = peppercorn.parse(self.request.POST.items())
843
843
844 if 'review_members' in controls:
844 if 'review_members' in controls:
845 self._update_reviewers(
845 self._update_reviewers(
846 pull_request_id, controls['review_members'],
846 pull_request_id, controls['review_members'],
847 pull_request.reviewer_data)
847 pull_request.reviewer_data)
848 elif str2bool(self.request.POST.get('update_commits', 'false')):
848 elif str2bool(self.request.POST.get('update_commits', 'false')):
849 self._update_commits(pull_request)
849 self._update_commits(pull_request)
850 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
850 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
851 self._edit_pull_request(pull_request)
851 self._edit_pull_request(pull_request)
852 else:
852 else:
853 raise HTTPBadRequest()
853 raise HTTPBadRequest()
854 return True
854 return True
855 raise HTTPForbidden()
855 raise HTTPForbidden()
856
856
857 def _edit_pull_request(self, pull_request):
857 def _edit_pull_request(self, pull_request):
858 _ = self.request.translate
858 _ = self.request.translate
859 try:
859 try:
860 PullRequestModel().edit(
860 PullRequestModel().edit(
861 pull_request, self.request.POST.get('title'),
861 pull_request, self.request.POST.get('title'),
862 self.request.POST.get('description'), self._rhodecode_user)
862 self.request.POST.get('description'), self._rhodecode_user)
863 except ValueError:
863 except ValueError:
864 msg = _(u'Cannot update closed pull requests.')
864 msg = _(u'Cannot update closed pull requests.')
865 h.flash(msg, category='error')
865 h.flash(msg, category='error')
866 return
866 return
867 else:
867 else:
868 Session().commit()
868 Session().commit()
869
869
870 msg = _(u'Pull request title & description updated.')
870 msg = _(u'Pull request title & description updated.')
871 h.flash(msg, category='success')
871 h.flash(msg, category='success')
872 return
872 return
873
873
874 def _update_commits(self, pull_request):
874 def _update_commits(self, pull_request):
875 _ = self.request.translate
875 _ = self.request.translate
876 resp = PullRequestModel().update_commits(pull_request)
876 resp = PullRequestModel().update_commits(pull_request)
877
877
878 if resp.executed:
878 if resp.executed:
879
879
880 if resp.target_changed and resp.source_changed:
880 if resp.target_changed and resp.source_changed:
881 changed = 'target and source repositories'
881 changed = 'target and source repositories'
882 elif resp.target_changed and not resp.source_changed:
882 elif resp.target_changed and not resp.source_changed:
883 changed = 'target repository'
883 changed = 'target repository'
884 elif not resp.target_changed and resp.source_changed:
884 elif not resp.target_changed and resp.source_changed:
885 changed = 'source repository'
885 changed = 'source repository'
886 else:
886 else:
887 changed = 'nothing'
887 changed = 'nothing'
888
888
889 msg = _(
889 msg = _(
890 u'Pull request updated to "{source_commit_id}" with '
890 u'Pull request updated to "{source_commit_id}" with '
891 u'{count_added} added, {count_removed} removed commits. '
891 u'{count_added} added, {count_removed} removed commits. '
892 u'Source of changes: {change_source}')
892 u'Source of changes: {change_source}')
893 msg = msg.format(
893 msg = msg.format(
894 source_commit_id=pull_request.source_ref_parts.commit_id,
894 source_commit_id=pull_request.source_ref_parts.commit_id,
895 count_added=len(resp.changes.added),
895 count_added=len(resp.changes.added),
896 count_removed=len(resp.changes.removed),
896 count_removed=len(resp.changes.removed),
897 change_source=changed)
897 change_source=changed)
898 h.flash(msg, category='success')
898 h.flash(msg, category='success')
899
899
900 channel = '/repo${}$/pr/{}'.format(
900 channel = '/repo${}$/pr/{}'.format(
901 pull_request.target_repo.repo_name,
901 pull_request.target_repo.repo_name,
902 pull_request.pull_request_id)
902 pull_request.pull_request_id)
903 message = msg + (
903 message = msg + (
904 ' - <a onclick="window.location.reload()">'
904 ' - <a onclick="window.location.reload()">'
905 '<strong>{}</strong></a>'.format(_('Reload page')))
905 '<strong>{}</strong></a>'.format(_('Reload page')))
906 channelstream.post_message(
906 channelstream.post_message(
907 channel, message, self._rhodecode_user.username,
907 channel, message, self._rhodecode_user.username,
908 registry=self.request.registry)
908 registry=self.request.registry)
909 else:
909 else:
910 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
910 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
911 warning_reasons = [
911 warning_reasons = [
912 UpdateFailureReason.NO_CHANGE,
912 UpdateFailureReason.NO_CHANGE,
913 UpdateFailureReason.WRONG_REF_TYPE,
913 UpdateFailureReason.WRONG_REF_TYPE,
914 ]
914 ]
915 category = 'warning' if resp.reason in warning_reasons else 'error'
915 category = 'warning' if resp.reason in warning_reasons else 'error'
916 h.flash(msg, category=category)
916 h.flash(msg, category=category)
917
917
918 @LoginRequired()
918 @LoginRequired()
919 @NotAnonymous()
919 @NotAnonymous()
920 @HasRepoPermissionAnyDecorator(
920 @HasRepoPermissionAnyDecorator(
921 'repository.read', 'repository.write', 'repository.admin')
921 'repository.read', 'repository.write', 'repository.admin')
922 @CSRFRequired()
922 @CSRFRequired()
923 @view_config(
923 @view_config(
924 route_name='pullrequest_merge', request_method='POST',
924 route_name='pullrequest_merge', request_method='POST',
925 renderer='json_ext')
925 renderer='json_ext')
926 def pull_request_merge(self):
926 def pull_request_merge(self):
927 """
927 """
928 Merge will perform a server-side merge of the specified
928 Merge will perform a server-side merge of the specified
929 pull request, if the pull request is approved and mergeable.
929 pull request, if the pull request is approved and mergeable.
930 After successful merging, the pull request is automatically
930 After successful merging, the pull request is automatically
931 closed, with a relevant comment.
931 closed, with a relevant comment.
932 """
932 """
933 pull_request_id = self.request.matchdict['pull_request_id']
933 pull_request_id = self.request.matchdict['pull_request_id']
934 pull_request = PullRequest.get_or_404(pull_request_id)
934 pull_request = PullRequest.get_or_404(pull_request_id)
935
935
936 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
936 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
937 merge_possible = not check.failed
937 merge_possible = not check.failed
938
938
939 for err_type, error_msg in check.errors:
939 for err_type, error_msg in check.errors:
940 h.flash(error_msg, category=err_type)
940 h.flash(error_msg, category=err_type)
941
941
942 if merge_possible:
942 if merge_possible:
943 log.debug("Pre-conditions checked, trying to merge.")
943 log.debug("Pre-conditions checked, trying to merge.")
944 extras = vcs_operation_context(
944 extras = vcs_operation_context(
945 self.request.environ, repo_name=pull_request.target_repo.repo_name,
945 self.request.environ, repo_name=pull_request.target_repo.repo_name,
946 username=self._rhodecode_db_user.username, action='push',
946 username=self._rhodecode_db_user.username, action='push',
947 scm=pull_request.target_repo.repo_type)
947 scm=pull_request.target_repo.repo_type)
948 self._merge_pull_request(
948 self._merge_pull_request(
949 pull_request, self._rhodecode_db_user, extras)
949 pull_request, self._rhodecode_db_user, extras)
950 else:
950 else:
951 log.debug("Pre-conditions failed, NOT merging.")
951 log.debug("Pre-conditions failed, NOT merging.")
952
952
953 raise HTTPFound(
953 raise HTTPFound(
954 h.route_path('pullrequest_show',
954 h.route_path('pullrequest_show',
955 repo_name=pull_request.target_repo.repo_name,
955 repo_name=pull_request.target_repo.repo_name,
956 pull_request_id=pull_request.pull_request_id))
956 pull_request_id=pull_request.pull_request_id))
957
957
958 def _merge_pull_request(self, pull_request, user, extras):
958 def _merge_pull_request(self, pull_request, user, extras):
959 _ = self.request.translate
959 _ = self.request.translate
960 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
960 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
961
961
962 if merge_resp.executed:
962 if merge_resp.executed:
963 log.debug("The merge was successful, closing the pull request.")
963 log.debug("The merge was successful, closing the pull request.")
964 PullRequestModel().close_pull_request(
964 PullRequestModel().close_pull_request(
965 pull_request.pull_request_id, user)
965 pull_request.pull_request_id, user)
966 Session().commit()
966 Session().commit()
967 msg = _('Pull request was successfully merged and closed.')
967 msg = _('Pull request was successfully merged and closed.')
968 h.flash(msg, category='success')
968 h.flash(msg, category='success')
969 else:
969 else:
970 log.debug(
970 log.debug(
971 "The merge was not successful. Merge response: %s",
971 "The merge was not successful. Merge response: %s",
972 merge_resp)
972 merge_resp)
973 msg = PullRequestModel().merge_status_message(
973 msg = PullRequestModel().merge_status_message(
974 merge_resp.failure_reason)
974 merge_resp.failure_reason)
975 h.flash(msg, category='error')
975 h.flash(msg, category='error')
976
976
977 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
977 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
978 _ = self.request.translate
978 _ = self.request.translate
979 get_default_reviewers_data, validate_default_reviewers = \
979 get_default_reviewers_data, validate_default_reviewers = \
980 PullRequestModel().get_reviewer_functions()
980 PullRequestModel().get_reviewer_functions()
981
981
982 try:
982 try:
983 reviewers = validate_default_reviewers(review_members, reviewer_rules)
983 reviewers = validate_default_reviewers(review_members, reviewer_rules)
984 except ValueError as e:
984 except ValueError as e:
985 log.error('Reviewers Validation: {}'.format(e))
985 log.error('Reviewers Validation: {}'.format(e))
986 h.flash(e, category='error')
986 h.flash(e, category='error')
987 return
987 return
988
988
989 PullRequestModel().update_reviewers(
989 PullRequestModel().update_reviewers(
990 pull_request_id, reviewers, self._rhodecode_user)
990 pull_request_id, reviewers, self._rhodecode_user)
991 h.flash(_('Pull request reviewers updated.'), category='success')
991 h.flash(_('Pull request reviewers updated.'), category='success')
992 Session().commit()
992 Session().commit()
993
993
994 @LoginRequired()
994 @LoginRequired()
995 @NotAnonymous()
995 @NotAnonymous()
996 @HasRepoPermissionAnyDecorator(
996 @HasRepoPermissionAnyDecorator(
997 'repository.read', 'repository.write', 'repository.admin')
997 'repository.read', 'repository.write', 'repository.admin')
998 @CSRFRequired()
998 @CSRFRequired()
999 @view_config(
999 @view_config(
1000 route_name='pullrequest_delete', request_method='POST',
1000 route_name='pullrequest_delete', request_method='POST',
1001 renderer='json_ext')
1001 renderer='json_ext')
1002 def pull_request_delete(self):
1002 def pull_request_delete(self):
1003 _ = self.request.translate
1003 _ = self.request.translate
1004
1004
1005 pull_request_id = self.request.matchdict['pull_request_id']
1005 pull_request_id = self.request.matchdict['pull_request_id']
1006 pull_request = PullRequest.get_or_404(pull_request_id)
1006 pull_request = PullRequest.get_or_404(pull_request_id)
1007
1007
1008 pr_closed = pull_request.is_closed()
1008 pr_closed = pull_request.is_closed()
1009 allowed_to_delete = PullRequestModel().check_user_delete(
1009 allowed_to_delete = PullRequestModel().check_user_delete(
1010 pull_request, self._rhodecode_user) and not pr_closed
1010 pull_request, self._rhodecode_user) and not pr_closed
1011
1011
1012 # only owner can delete it !
1012 # only owner can delete it !
1013 if allowed_to_delete:
1013 if allowed_to_delete:
1014 PullRequestModel().delete(pull_request, self._rhodecode_user)
1014 PullRequestModel().delete(pull_request, self._rhodecode_user)
1015 Session().commit()
1015 Session().commit()
1016 h.flash(_('Successfully deleted pull request'),
1016 h.flash(_('Successfully deleted pull request'),
1017 category='success')
1017 category='success')
1018 raise HTTPFound(h.route_path('my_account_pullrequests'))
1018 raise HTTPFound(h.route_path('my_account_pullrequests'))
1019
1019
1020 log.warning('user %s tried to delete pull request without access',
1020 log.warning('user %s tried to delete pull request without access',
1021 self._rhodecode_user)
1021 self._rhodecode_user)
1022 raise HTTPNotFound()
1022 raise HTTPNotFound()
1023
1023
1024 @LoginRequired()
1024 @LoginRequired()
1025 @NotAnonymous()
1025 @NotAnonymous()
1026 @HasRepoPermissionAnyDecorator(
1026 @HasRepoPermissionAnyDecorator(
1027 'repository.read', 'repository.write', 'repository.admin')
1027 'repository.read', 'repository.write', 'repository.admin')
1028 @CSRFRequired()
1028 @CSRFRequired()
1029 @view_config(
1029 @view_config(
1030 route_name='pullrequest_comment_create', request_method='POST',
1030 route_name='pullrequest_comment_create', request_method='POST',
1031 renderer='json_ext')
1031 renderer='json_ext')
1032 def pull_request_comment_create(self):
1032 def pull_request_comment_create(self):
1033 _ = self.request.translate
1033 _ = self.request.translate
1034 pull_request_id = self.request.matchdict['pull_request_id']
1034 pull_request_id = self.request.matchdict['pull_request_id']
1035 pull_request = PullRequest.get_or_404(pull_request_id)
1035 pull_request = PullRequest.get_or_404(pull_request_id)
1036 if pull_request.is_closed():
1036 if pull_request.is_closed():
1037 log.debug('comment: forbidden because pull request is closed')
1037 log.debug('comment: forbidden because pull request is closed')
1038 raise HTTPForbidden()
1038 raise HTTPForbidden()
1039
1039
1040 c = self.load_default_context()
1040 c = self.load_default_context()
1041
1041
1042 status = self.request.POST.get('changeset_status', None)
1042 status = self.request.POST.get('changeset_status', None)
1043 text = self.request.POST.get('text')
1043 text = self.request.POST.get('text')
1044 comment_type = self.request.POST.get('comment_type')
1044 comment_type = self.request.POST.get('comment_type')
1045 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1045 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1046 close_pull_request = self.request.POST.get('close_pull_request')
1046 close_pull_request = self.request.POST.get('close_pull_request')
1047
1047
1048 # the logic here should work like following, if we submit close
1048 # the logic here should work like following, if we submit close
1049 # pr comment, use `close_pull_request_with_comment` function
1049 # pr comment, use `close_pull_request_with_comment` function
1050 # else handle regular comment logic
1050 # else handle regular comment logic
1051
1051
1052 if close_pull_request:
1052 if close_pull_request:
1053 # only owner or admin or person with write permissions
1053 # only owner or admin or person with write permissions
1054 allowed_to_close = PullRequestModel().check_user_update(
1054 allowed_to_close = PullRequestModel().check_user_update(
1055 pull_request, self._rhodecode_user)
1055 pull_request, self._rhodecode_user)
1056 if not allowed_to_close:
1056 if not allowed_to_close:
1057 log.debug('comment: forbidden because not allowed to close '
1057 log.debug('comment: forbidden because not allowed to close '
1058 'pull request %s', pull_request_id)
1058 'pull request %s', pull_request_id)
1059 raise HTTPForbidden()
1059 raise HTTPForbidden()
1060 comment, status = PullRequestModel().close_pull_request_with_comment(
1060 comment, status = PullRequestModel().close_pull_request_with_comment(
1061 pull_request, self._rhodecode_user, self.db_repo, message=text)
1061 pull_request, self._rhodecode_user, self.db_repo, message=text)
1062 Session().flush()
1062 Session().flush()
1063 events.trigger(
1063 events.trigger(
1064 events.PullRequestCommentEvent(pull_request, comment))
1064 events.PullRequestCommentEvent(pull_request, comment))
1065
1065
1066 else:
1066 else:
1067 # regular comment case, could be inline, or one with status.
1067 # regular comment case, could be inline, or one with status.
1068 # for that one we check also permissions
1068 # for that one we check also permissions
1069
1069
1070 allowed_to_change_status = PullRequestModel().check_user_change_status(
1070 allowed_to_change_status = PullRequestModel().check_user_change_status(
1071 pull_request, self._rhodecode_user)
1071 pull_request, self._rhodecode_user)
1072
1072
1073 if status and allowed_to_change_status:
1073 if status and allowed_to_change_status:
1074 message = (_('Status change %(transition_icon)s %(status)s')
1074 message = (_('Status change %(transition_icon)s %(status)s')
1075 % {'transition_icon': '>',
1075 % {'transition_icon': '>',
1076 'status': ChangesetStatus.get_status_lbl(status)})
1076 'status': ChangesetStatus.get_status_lbl(status)})
1077 text = text or message
1077 text = text or message
1078
1078
1079 comment = CommentsModel().create(
1079 comment = CommentsModel().create(
1080 text=text,
1080 text=text,
1081 repo=self.db_repo.repo_id,
1081 repo=self.db_repo.repo_id,
1082 user=self._rhodecode_user.user_id,
1082 user=self._rhodecode_user.user_id,
1083 pull_request=pull_request_id,
1083 pull_request=pull_request_id,
1084 f_path=self.request.POST.get('f_path'),
1084 f_path=self.request.POST.get('f_path'),
1085 line_no=self.request.POST.get('line'),
1085 line_no=self.request.POST.get('line'),
1086 status_change=(ChangesetStatus.get_status_lbl(status)
1086 status_change=(ChangesetStatus.get_status_lbl(status)
1087 if status and allowed_to_change_status else None),
1087 if status and allowed_to_change_status else None),
1088 status_change_type=(status
1088 status_change_type=(status
1089 if status and allowed_to_change_status else None),
1089 if status and allowed_to_change_status else None),
1090 comment_type=comment_type,
1090 comment_type=comment_type,
1091 resolves_comment_id=resolves_comment_id
1091 resolves_comment_id=resolves_comment_id
1092 )
1092 )
1093
1093
1094 if allowed_to_change_status:
1094 if allowed_to_change_status:
1095 # calculate old status before we change it
1095 # calculate old status before we change it
1096 old_calculated_status = pull_request.calculated_review_status()
1096 old_calculated_status = pull_request.calculated_review_status()
1097
1097
1098 # get status if set !
1098 # get status if set !
1099 if status:
1099 if status:
1100 ChangesetStatusModel().set_status(
1100 ChangesetStatusModel().set_status(
1101 self.db_repo.repo_id,
1101 self.db_repo.repo_id,
1102 status,
1102 status,
1103 self._rhodecode_user.user_id,
1103 self._rhodecode_user.user_id,
1104 comment,
1104 comment,
1105 pull_request=pull_request_id
1105 pull_request=pull_request_id
1106 )
1106 )
1107
1107
1108 Session().flush()
1108 Session().flush()
1109 events.trigger(
1109 events.trigger(
1110 events.PullRequestCommentEvent(pull_request, comment))
1110 events.PullRequestCommentEvent(pull_request, comment))
1111
1111
1112 # we now calculate the status of pull request, and based on that
1112 # we now calculate the status of pull request, and based on that
1113 # calculation we set the commits status
1113 # calculation we set the commits status
1114 calculated_status = pull_request.calculated_review_status()
1114 calculated_status = pull_request.calculated_review_status()
1115 if old_calculated_status != calculated_status:
1115 if old_calculated_status != calculated_status:
1116 PullRequestModel()._trigger_pull_request_hook(
1116 PullRequestModel()._trigger_pull_request_hook(
1117 pull_request, self._rhodecode_user, 'review_status_change')
1117 pull_request, self._rhodecode_user, 'review_status_change')
1118
1118
1119 Session().commit()
1119 Session().commit()
1120
1120
1121 data = {
1121 data = {
1122 'target_id': h.safeid(h.safe_unicode(
1122 'target_id': h.safeid(h.safe_unicode(
1123 self.request.POST.get('f_path'))),
1123 self.request.POST.get('f_path'))),
1124 }
1124 }
1125 if comment:
1125 if comment:
1126 c.co = comment
1126 c.co = comment
1127 rendered_comment = render(
1127 rendered_comment = render(
1128 'rhodecode:templates/changeset/changeset_comment_block.mako',
1128 'rhodecode:templates/changeset/changeset_comment_block.mako',
1129 self._get_template_context(c), self.request)
1129 self._get_template_context(c), self.request)
1130
1130
1131 data.update(comment.get_dict())
1131 data.update(comment.get_dict())
1132 data.update({'rendered_text': rendered_comment})
1132 data.update({'rendered_text': rendered_comment})
1133
1133
1134 return data
1134 return data
1135
1135
1136 @LoginRequired()
1136 @LoginRequired()
1137 @NotAnonymous()
1137 @NotAnonymous()
1138 @HasRepoPermissionAnyDecorator(
1138 @HasRepoPermissionAnyDecorator(
1139 'repository.read', 'repository.write', 'repository.admin')
1139 'repository.read', 'repository.write', 'repository.admin')
1140 @CSRFRequired()
1140 @CSRFRequired()
1141 @view_config(
1141 @view_config(
1142 route_name='pullrequest_comment_delete', request_method='POST',
1142 route_name='pullrequest_comment_delete', request_method='POST',
1143 renderer='json_ext')
1143 renderer='json_ext')
1144 def pull_request_comment_delete(self):
1144 def pull_request_comment_delete(self):
1145 commit_id = self.request.matchdict['commit_id']
1145 pull_request_id = self.request.matchdict['pull_request_id']
1146 comment_id = self.request.matchdict['comment_id']
1146 comment_id = self.request.matchdict['comment_id']
1147 pull_request_id = self.request.matchdict['pull_request_id']
1148
1147
1149 pull_request = PullRequest.get_or_404(pull_request_id)
1148 pull_request = PullRequest.get_or_404(pull_request_id)
1150 if pull_request.is_closed():
1149 if pull_request.is_closed():
1151 log.debug('comment: forbidden because pull request is closed')
1150 log.debug('comment: forbidden because pull request is closed')
1152 raise HTTPForbidden()
1151 raise HTTPForbidden()
1153
1152
1154 comment = ChangesetComment.get_or_404(comment_id)
1153 comment = ChangesetComment.get_or_404(comment_id)
1155 if not comment:
1154 if not comment:
1156 log.debug('Comment with id:%s not found, skipping', comment_id)
1155 log.debug('Comment with id:%s not found, skipping', comment_id)
1157 # comment already deleted in another call probably
1156 # comment already deleted in another call probably
1158 return True
1157 return True
1159
1158
1160 if comment.pull_request.is_closed():
1159 if comment.pull_request.is_closed():
1161 # don't allow deleting comments on closed pull request
1160 # don't allow deleting comments on closed pull request
1162 raise HTTPForbidden()
1161 raise HTTPForbidden()
1163
1162
1164 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1163 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1165 super_admin = h.HasPermissionAny('hg.admin')()
1164 super_admin = h.HasPermissionAny('hg.admin')()
1166 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1165 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1167 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1166 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1168 comment_repo_admin = is_repo_admin and is_repo_comment
1167 comment_repo_admin = is_repo_admin and is_repo_comment
1169
1168
1170 if super_admin or comment_owner or comment_repo_admin:
1169 if super_admin or comment_owner or comment_repo_admin:
1171 old_calculated_status = comment.pull_request.calculated_review_status()
1170 old_calculated_status = comment.pull_request.calculated_review_status()
1172 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1171 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1173 Session().commit()
1172 Session().commit()
1174 calculated_status = comment.pull_request.calculated_review_status()
1173 calculated_status = comment.pull_request.calculated_review_status()
1175 if old_calculated_status != calculated_status:
1174 if old_calculated_status != calculated_status:
1176 PullRequestModel()._trigger_pull_request_hook(
1175 PullRequestModel()._trigger_pull_request_hook(
1177 comment.pull_request, self._rhodecode_user, 'review_status_change')
1176 comment.pull_request, self._rhodecode_user, 'review_status_change')
1178 return True
1177 return True
1179 else:
1178 else:
1180 log.warning('No permissions for user %s to delete comment_id: %s',
1179 log.warning('No permissions for user %s to delete comment_id: %s',
1181 self._rhodecode_db_user, comment_id)
1180 self._rhodecode_db_user, comment_id)
1182 raise HTTPNotFound()
1181 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now