##// END OF EJS Templates
pull-requests: use proper diff calculation for versioning of PRs.
marcink -
r4426:f4c1c6eb default
parent child Browse files
Show More
@@ -1,1435 +1,1652 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
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 'repo_commits': '/{repo_name}/commits',
44 'repo_commits': '/{repo_name}/commits',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 }[name].format(**kwargs)
59 }[name].format(**kwargs)
60
60
61 if params:
61 if params:
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 return base_url
63 return base_url
64
64
65
65
66 @pytest.mark.usefixtures('app', 'autologin_user')
66 @pytest.mark.usefixtures('app', 'autologin_user')
67 @pytest.mark.backends("git", "hg")
67 @pytest.mark.backends("git", "hg")
68 class TestPullrequestsView(object):
68 class TestPullrequestsView(object):
69
69
70 def test_index(self, backend):
70 def test_index(self, backend):
71 self.app.get(route_path(
71 self.app.get(route_path(
72 'pullrequest_new',
72 'pullrequest_new',
73 repo_name=backend.repo_name))
73 repo_name=backend.repo_name))
74
74
75 def test_option_menu_create_pull_request_exists(self, backend):
75 def test_option_menu_create_pull_request_exists(self, backend):
76 repo_name = backend.repo_name
76 repo_name = backend.repo_name
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78
78
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 'pullrequest_new', repo_name=repo_name)
80 'pullrequest_new', repo_name=repo_name)
81 response.mustcontain(create_pr_link)
81 response.mustcontain(create_pr_link)
82
82
83 def test_create_pr_form_with_raw_commit_id(self, backend):
83 def test_create_pr_form_with_raw_commit_id(self, backend):
84 repo = backend.repo
84 repo = backend.repo
85
85
86 self.app.get(
86 self.app.get(
87 route_path('pullrequest_new', repo_name=repo.repo_name,
87 route_path('pullrequest_new', repo_name=repo.repo_name,
88 commit=repo.get_commit().raw_id),
88 commit=repo.get_commit().raw_id),
89 status=200)
89 status=200)
90
90
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 pull_request = pr_util.create_pull_request(
94 pull_request = pr_util.create_pull_request(
95 mergeable=pr_merge_enabled, enable_notifications=False)
95 mergeable=pr_merge_enabled, enable_notifications=False)
96
96
97 response = self.app.get(route_path(
97 response = self.app.get(route_path(
98 'pullrequest_show',
98 'pullrequest_show',
99 repo_name=pull_request.target_repo.scm_instance().name,
99 repo_name=pull_request.target_repo.scm_instance().name,
100 pull_request_id=pull_request.pull_request_id,
100 pull_request_id=pull_request.pull_request_id,
101 params={'range-diff': range_diff}))
101 params={'range-diff': range_diff}))
102
102
103 for commit_id in pull_request.revisions:
103 for commit_id in pull_request.revisions:
104 response.mustcontain(commit_id)
104 response.mustcontain(commit_id)
105
105
106 response.mustcontain(pull_request.target_ref_parts.type)
106 response.mustcontain(pull_request.target_ref_parts.type)
107 response.mustcontain(pull_request.target_ref_parts.name)
107 response.mustcontain(pull_request.target_ref_parts.name)
108
108
109 response.mustcontain('class="pull-request-merge"')
109 response.mustcontain('class="pull-request-merge"')
110
110
111 if pr_merge_enabled:
111 if pr_merge_enabled:
112 response.mustcontain('Pull request reviewer approval is pending')
112 response.mustcontain('Pull request reviewer approval is pending')
113 else:
113 else:
114 response.mustcontain('Server-side pull request merging is disabled.')
114 response.mustcontain('Server-side pull request merging is disabled.')
115
115
116 if range_diff == "1":
116 if range_diff == "1":
117 response.mustcontain('Turn off: Show the diff as commit range')
117 response.mustcontain('Turn off: Show the diff as commit range')
118
118
119 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
121 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123
124 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
127
128 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131
132 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
135 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
138
139 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
144
145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
153 ('source_repo', source.repo_name),
154 ('source_ref', source_ref),
155 ('target_repo', target.repo_name),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
175 ('user', ''),
176 ('csrf_token', csrf_token),
177 ],
178 status=302)
179
180 location = response.headers['Location']
181
182 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
185
186 pull_request_id = pull_request.pull_request_id
187
188 # Show initial version of PR
189 response = self.app.get(
190 route_path('pullrequest_show',
191 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
193
194 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
198
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
203
204 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
208 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
212 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
216 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
220 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
224 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
228 # Show final version !
229 response = self.app.get(
230 route_path('pullrequest_show',
231 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
233
234 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
237
238 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
243
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
249 # fetch versions
250 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
253
254 # show v1,v2,v3,v4
255 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
257
258 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
261
262 commit_tests = {
263 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
269 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
275 for idx, ver in enumerate(versions, 1):
276
277 response = self.app.get(
278 route_path('pullrequest_show',
279 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
281 params={'version': ver}))
282
283 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
285
286 yes, no = commit_tests[idx]
287 for y in yes:
288 response.mustcontain(y)
289 for n in no:
290 response.mustcontain(no=n)
291
292 yes, no = diff_tests[idx]
293 for y in yes:
294 response.mustcontain(cb_line(y))
295 for n in no:
296 response.mustcontain(no=n)
297
298 # show diff between versions
299 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
304 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
306
307 to_ver = ver+1
308 if idx == 3:
309 to_ver = 'latest'
310
311 response = self.app.get(
312 route_path('pullrequest_show',
313 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
316
317 response.mustcontain(no=['rhodecode diff rendering error'])
318
319 for a in adds:
320 response.mustcontain(cb_line(a))
321 for c in context:
322 response.mustcontain(cb_context(c))
323
324 # test version v2 -> v3
325 response = self.app.get(
326 route_path('pullrequest_show',
327 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
330
331 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
335
119 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
120 # Logout
337 # Logout
121 response = self.app.post(
338 response = self.app.post(
122 h.route_path('logout'),
339 h.route_path('logout'),
123 params={'csrf_token': csrf_token})
340 params={'csrf_token': csrf_token})
124 # Login as regular user
341 # Login as regular user
125 response = self.app.post(h.route_path('login'),
342 response = self.app.post(h.route_path('login'),
126 {'username': TEST_USER_REGULAR_LOGIN,
343 {'username': TEST_USER_REGULAR_LOGIN,
127 'password': 'test12'})
344 'password': 'test12'})
128
345
129 pull_request = pr_util.create_pull_request(
346 pull_request = pr_util.create_pull_request(
130 author=TEST_USER_REGULAR_LOGIN)
347 author=TEST_USER_REGULAR_LOGIN)
131
348
132 response = self.app.get(route_path(
349 response = self.app.get(route_path(
133 'pullrequest_show',
350 'pullrequest_show',
134 repo_name=pull_request.target_repo.scm_instance().name,
351 repo_name=pull_request.target_repo.scm_instance().name,
135 pull_request_id=pull_request.pull_request_id))
352 pull_request_id=pull_request.pull_request_id))
136
353
137 response.mustcontain('Server-side pull request merging is disabled.')
354 response.mustcontain('Server-side pull request merging is disabled.')
138
355
139 assert_response = response.assert_response()
356 assert_response = response.assert_response()
140 # for regular user without a merge permissions, we don't see it
357 # for regular user without a merge permissions, we don't see it
141 assert_response.no_element_exists('#close-pull-request-action')
358 assert_response.no_element_exists('#close-pull-request-action')
142
359
143 user_util.grant_user_permission_to_repo(
360 user_util.grant_user_permission_to_repo(
144 pull_request.target_repo,
361 pull_request.target_repo,
145 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
146 'repository.write')
363 'repository.write')
147 response = self.app.get(route_path(
364 response = self.app.get(route_path(
148 'pullrequest_show',
365 'pullrequest_show',
149 repo_name=pull_request.target_repo.scm_instance().name,
366 repo_name=pull_request.target_repo.scm_instance().name,
150 pull_request_id=pull_request.pull_request_id))
367 pull_request_id=pull_request.pull_request_id))
151
368
152 response.mustcontain('Server-side pull request merging is disabled.')
369 response.mustcontain('Server-side pull request merging is disabled.')
153
370
154 assert_response = response.assert_response()
371 assert_response = response.assert_response()
155 # now regular user has a merge permissions, we have CLOSE button
372 # now regular user has a merge permissions, we have CLOSE button
156 assert_response.one_element_exists('#close-pull-request-action')
373 assert_response.one_element_exists('#close-pull-request-action')
157
374
158 def test_show_invalid_commit_id(self, pr_util):
375 def test_show_invalid_commit_id(self, pr_util):
159 # Simulating invalid revisions which will cause a lookup error
376 # Simulating invalid revisions which will cause a lookup error
160 pull_request = pr_util.create_pull_request()
377 pull_request = pr_util.create_pull_request()
161 pull_request.revisions = ['invalid']
378 pull_request.revisions = ['invalid']
162 Session().add(pull_request)
379 Session().add(pull_request)
163 Session().commit()
380 Session().commit()
164
381
165 response = self.app.get(route_path(
382 response = self.app.get(route_path(
166 'pullrequest_show',
383 'pullrequest_show',
167 repo_name=pull_request.target_repo.scm_instance().name,
384 repo_name=pull_request.target_repo.scm_instance().name,
168 pull_request_id=pull_request.pull_request_id))
385 pull_request_id=pull_request.pull_request_id))
169
386
170 for commit_id in pull_request.revisions:
387 for commit_id in pull_request.revisions:
171 response.mustcontain(commit_id)
388 response.mustcontain(commit_id)
172
389
173 def test_show_invalid_source_reference(self, pr_util):
390 def test_show_invalid_source_reference(self, pr_util):
174 pull_request = pr_util.create_pull_request()
391 pull_request = pr_util.create_pull_request()
175 pull_request.source_ref = 'branch:b:invalid'
392 pull_request.source_ref = 'branch:b:invalid'
176 Session().add(pull_request)
393 Session().add(pull_request)
177 Session().commit()
394 Session().commit()
178
395
179 self.app.get(route_path(
396 self.app.get(route_path(
180 'pullrequest_show',
397 'pullrequest_show',
181 repo_name=pull_request.target_repo.scm_instance().name,
398 repo_name=pull_request.target_repo.scm_instance().name,
182 pull_request_id=pull_request.pull_request_id))
399 pull_request_id=pull_request.pull_request_id))
183
400
184 def test_edit_title_description(self, pr_util, csrf_token):
401 def test_edit_title_description(self, pr_util, csrf_token):
185 pull_request = pr_util.create_pull_request()
402 pull_request = pr_util.create_pull_request()
186 pull_request_id = pull_request.pull_request_id
403 pull_request_id = pull_request.pull_request_id
187
404
188 response = self.app.post(
405 response = self.app.post(
189 route_path('pullrequest_update',
406 route_path('pullrequest_update',
190 repo_name=pull_request.target_repo.repo_name,
407 repo_name=pull_request.target_repo.repo_name,
191 pull_request_id=pull_request_id),
408 pull_request_id=pull_request_id),
192 params={
409 params={
193 'edit_pull_request': 'true',
410 'edit_pull_request': 'true',
194 'title': 'New title',
411 'title': 'New title',
195 'description': 'New description',
412 'description': 'New description',
196 'csrf_token': csrf_token})
413 'csrf_token': csrf_token})
197
414
198 assert_session_flash(
415 assert_session_flash(
199 response, u'Pull request title & description updated.',
416 response, u'Pull request title & description updated.',
200 category='success')
417 category='success')
201
418
202 pull_request = PullRequest.get(pull_request_id)
419 pull_request = PullRequest.get(pull_request_id)
203 assert pull_request.title == 'New title'
420 assert pull_request.title == 'New title'
204 assert pull_request.description == 'New description'
421 assert pull_request.description == 'New description'
205
422
206 def test_edit_title_description_closed(self, pr_util, csrf_token):
423 def test_edit_title_description_closed(self, pr_util, csrf_token):
207 pull_request = pr_util.create_pull_request()
424 pull_request = pr_util.create_pull_request()
208 pull_request_id = pull_request.pull_request_id
425 pull_request_id = pull_request.pull_request_id
209 repo_name = pull_request.target_repo.repo_name
426 repo_name = pull_request.target_repo.repo_name
210 pr_util.close()
427 pr_util.close()
211
428
212 response = self.app.post(
429 response = self.app.post(
213 route_path('pullrequest_update',
430 route_path('pullrequest_update',
214 repo_name=repo_name, pull_request_id=pull_request_id),
431 repo_name=repo_name, pull_request_id=pull_request_id),
215 params={
432 params={
216 'edit_pull_request': 'true',
433 'edit_pull_request': 'true',
217 'title': 'New title',
434 'title': 'New title',
218 'description': 'New description',
435 'description': 'New description',
219 'csrf_token': csrf_token}, status=200)
436 'csrf_token': csrf_token}, status=200)
220 assert_session_flash(
437 assert_session_flash(
221 response, u'Cannot update closed pull requests.',
438 response, u'Cannot update closed pull requests.',
222 category='error')
439 category='error')
223
440
224 def test_update_invalid_source_reference(self, pr_util, csrf_token):
441 def test_update_invalid_source_reference(self, pr_util, csrf_token):
225 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
442 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
226
443
227 pull_request = pr_util.create_pull_request()
444 pull_request = pr_util.create_pull_request()
228 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
445 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
229 Session().add(pull_request)
446 Session().add(pull_request)
230 Session().commit()
447 Session().commit()
231
448
232 pull_request_id = pull_request.pull_request_id
449 pull_request_id = pull_request.pull_request_id
233
450
234 response = self.app.post(
451 response = self.app.post(
235 route_path('pullrequest_update',
452 route_path('pullrequest_update',
236 repo_name=pull_request.target_repo.repo_name,
453 repo_name=pull_request.target_repo.repo_name,
237 pull_request_id=pull_request_id),
454 pull_request_id=pull_request_id),
238 params={'update_commits': 'true', 'csrf_token': csrf_token})
455 params={'update_commits': 'true', 'csrf_token': csrf_token})
239
456
240 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
457 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
241 UpdateFailureReason.MISSING_SOURCE_REF])
458 UpdateFailureReason.MISSING_SOURCE_REF])
242 assert_session_flash(response, expected_msg, category='error')
459 assert_session_flash(response, expected_msg, category='error')
243
460
244 def test_missing_target_reference(self, pr_util, csrf_token):
461 def test_missing_target_reference(self, pr_util, csrf_token):
245 from rhodecode.lib.vcs.backends.base import MergeFailureReason
462 from rhodecode.lib.vcs.backends.base import MergeFailureReason
246 pull_request = pr_util.create_pull_request(
463 pull_request = pr_util.create_pull_request(
247 approved=True, mergeable=True)
464 approved=True, mergeable=True)
248 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
465 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
249 pull_request.target_ref = unicode_reference
466 pull_request.target_ref = unicode_reference
250 Session().add(pull_request)
467 Session().add(pull_request)
251 Session().commit()
468 Session().commit()
252
469
253 pull_request_id = pull_request.pull_request_id
470 pull_request_id = pull_request.pull_request_id
254 pull_request_url = route_path(
471 pull_request_url = route_path(
255 'pullrequest_show',
472 'pullrequest_show',
256 repo_name=pull_request.target_repo.repo_name,
473 repo_name=pull_request.target_repo.repo_name,
257 pull_request_id=pull_request_id)
474 pull_request_id=pull_request_id)
258
475
259 response = self.app.get(pull_request_url)
476 response = self.app.get(pull_request_url)
260 target_ref_id = 'invalid-branch'
477 target_ref_id = 'invalid-branch'
261 merge_resp = MergeResponse(
478 merge_resp = MergeResponse(
262 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
479 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
263 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
480 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
264 response.assert_response().element_contains(
481 response.assert_response().element_contains(
265 'div[data-role="merge-message"]', merge_resp.merge_status_message)
482 'div[data-role="merge-message"]', merge_resp.merge_status_message)
266
483
267 def test_comment_and_close_pull_request_custom_message_approved(
484 def test_comment_and_close_pull_request_custom_message_approved(
268 self, pr_util, csrf_token, xhr_header):
485 self, pr_util, csrf_token, xhr_header):
269
486
270 pull_request = pr_util.create_pull_request(approved=True)
487 pull_request = pr_util.create_pull_request(approved=True)
271 pull_request_id = pull_request.pull_request_id
488 pull_request_id = pull_request.pull_request_id
272 author = pull_request.user_id
489 author = pull_request.user_id
273 repo = pull_request.target_repo.repo_id
490 repo = pull_request.target_repo.repo_id
274
491
275 self.app.post(
492 self.app.post(
276 route_path('pullrequest_comment_create',
493 route_path('pullrequest_comment_create',
277 repo_name=pull_request.target_repo.scm_instance().name,
494 repo_name=pull_request.target_repo.scm_instance().name,
278 pull_request_id=pull_request_id),
495 pull_request_id=pull_request_id),
279 params={
496 params={
280 'close_pull_request': '1',
497 'close_pull_request': '1',
281 'text': 'Closing a PR',
498 'text': 'Closing a PR',
282 'csrf_token': csrf_token},
499 'csrf_token': csrf_token},
283 extra_environ=xhr_header,)
500 extra_environ=xhr_header,)
284
501
285 journal = UserLog.query()\
502 journal = UserLog.query()\
286 .filter(UserLog.user_id == author)\
503 .filter(UserLog.user_id == author)\
287 .filter(UserLog.repository_id == repo) \
504 .filter(UserLog.repository_id == repo) \
288 .order_by(UserLog.user_log_id.asc()) \
505 .order_by(UserLog.user_log_id.asc()) \
289 .all()
506 .all()
290 assert journal[-1].action == 'repo.pull_request.close'
507 assert journal[-1].action == 'repo.pull_request.close'
291
508
292 pull_request = PullRequest.get(pull_request_id)
509 pull_request = PullRequest.get(pull_request_id)
293 assert pull_request.is_closed()
510 assert pull_request.is_closed()
294
511
295 status = ChangesetStatusModel().get_status(
512 status = ChangesetStatusModel().get_status(
296 pull_request.source_repo, pull_request=pull_request)
513 pull_request.source_repo, pull_request=pull_request)
297 assert status == ChangesetStatus.STATUS_APPROVED
514 assert status == ChangesetStatus.STATUS_APPROVED
298 comments = ChangesetComment().query() \
515 comments = ChangesetComment().query() \
299 .filter(ChangesetComment.pull_request == pull_request) \
516 .filter(ChangesetComment.pull_request == pull_request) \
300 .order_by(ChangesetComment.comment_id.asc())\
517 .order_by(ChangesetComment.comment_id.asc())\
301 .all()
518 .all()
302 assert comments[-1].text == 'Closing a PR'
519 assert comments[-1].text == 'Closing a PR'
303
520
304 def test_comment_force_close_pull_request_rejected(
521 def test_comment_force_close_pull_request_rejected(
305 self, pr_util, csrf_token, xhr_header):
522 self, pr_util, csrf_token, xhr_header):
306 pull_request = pr_util.create_pull_request()
523 pull_request = pr_util.create_pull_request()
307 pull_request_id = pull_request.pull_request_id
524 pull_request_id = pull_request.pull_request_id
308 PullRequestModel().update_reviewers(
525 PullRequestModel().update_reviewers(
309 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
526 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
310 pull_request.author)
527 pull_request.author)
311 author = pull_request.user_id
528 author = pull_request.user_id
312 repo = pull_request.target_repo.repo_id
529 repo = pull_request.target_repo.repo_id
313
530
314 self.app.post(
531 self.app.post(
315 route_path('pullrequest_comment_create',
532 route_path('pullrequest_comment_create',
316 repo_name=pull_request.target_repo.scm_instance().name,
533 repo_name=pull_request.target_repo.scm_instance().name,
317 pull_request_id=pull_request_id),
534 pull_request_id=pull_request_id),
318 params={
535 params={
319 'close_pull_request': '1',
536 'close_pull_request': '1',
320 'csrf_token': csrf_token},
537 'csrf_token': csrf_token},
321 extra_environ=xhr_header)
538 extra_environ=xhr_header)
322
539
323 pull_request = PullRequest.get(pull_request_id)
540 pull_request = PullRequest.get(pull_request_id)
324
541
325 journal = UserLog.query()\
542 journal = UserLog.query()\
326 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
543 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
327 .order_by(UserLog.user_log_id.asc()) \
544 .order_by(UserLog.user_log_id.asc()) \
328 .all()
545 .all()
329 assert journal[-1].action == 'repo.pull_request.close'
546 assert journal[-1].action == 'repo.pull_request.close'
330
547
331 # check only the latest status, not the review status
548 # check only the latest status, not the review status
332 status = ChangesetStatusModel().get_status(
549 status = ChangesetStatusModel().get_status(
333 pull_request.source_repo, pull_request=pull_request)
550 pull_request.source_repo, pull_request=pull_request)
334 assert status == ChangesetStatus.STATUS_REJECTED
551 assert status == ChangesetStatus.STATUS_REJECTED
335
552
336 def test_comment_and_close_pull_request(
553 def test_comment_and_close_pull_request(
337 self, pr_util, csrf_token, xhr_header):
554 self, pr_util, csrf_token, xhr_header):
338 pull_request = pr_util.create_pull_request()
555 pull_request = pr_util.create_pull_request()
339 pull_request_id = pull_request.pull_request_id
556 pull_request_id = pull_request.pull_request_id
340
557
341 response = self.app.post(
558 response = self.app.post(
342 route_path('pullrequest_comment_create',
559 route_path('pullrequest_comment_create',
343 repo_name=pull_request.target_repo.scm_instance().name,
560 repo_name=pull_request.target_repo.scm_instance().name,
344 pull_request_id=pull_request.pull_request_id),
561 pull_request_id=pull_request.pull_request_id),
345 params={
562 params={
346 'close_pull_request': 'true',
563 'close_pull_request': 'true',
347 'csrf_token': csrf_token},
564 'csrf_token': csrf_token},
348 extra_environ=xhr_header)
565 extra_environ=xhr_header)
349
566
350 assert response.json
567 assert response.json
351
568
352 pull_request = PullRequest.get(pull_request_id)
569 pull_request = PullRequest.get(pull_request_id)
353 assert pull_request.is_closed()
570 assert pull_request.is_closed()
354
571
355 # check only the latest status, not the review status
572 # check only the latest status, not the review status
356 status = ChangesetStatusModel().get_status(
573 status = ChangesetStatusModel().get_status(
357 pull_request.source_repo, pull_request=pull_request)
574 pull_request.source_repo, pull_request=pull_request)
358 assert status == ChangesetStatus.STATUS_REJECTED
575 assert status == ChangesetStatus.STATUS_REJECTED
359
576
360 def test_comment_and_close_pull_request_try_edit_comment(
577 def test_comment_and_close_pull_request_try_edit_comment(
361 self, pr_util, csrf_token, xhr_header
578 self, pr_util, csrf_token, xhr_header
362 ):
579 ):
363 pull_request = pr_util.create_pull_request()
580 pull_request = pr_util.create_pull_request()
364 pull_request_id = pull_request.pull_request_id
581 pull_request_id = pull_request.pull_request_id
365 target_scm = pull_request.target_repo.scm_instance()
582 target_scm = pull_request.target_repo.scm_instance()
366 target_scm_name = target_scm.name
583 target_scm_name = target_scm.name
367
584
368 response = self.app.post(
585 response = self.app.post(
369 route_path(
586 route_path(
370 'pullrequest_comment_create',
587 'pullrequest_comment_create',
371 repo_name=target_scm_name,
588 repo_name=target_scm_name,
372 pull_request_id=pull_request_id,
589 pull_request_id=pull_request_id,
373 ),
590 ),
374 params={
591 params={
375 'close_pull_request': 'true',
592 'close_pull_request': 'true',
376 'csrf_token': csrf_token,
593 'csrf_token': csrf_token,
377 },
594 },
378 extra_environ=xhr_header)
595 extra_environ=xhr_header)
379
596
380 assert response.json
597 assert response.json
381
598
382 pull_request = PullRequest.get(pull_request_id)
599 pull_request = PullRequest.get(pull_request_id)
383 target_scm = pull_request.target_repo.scm_instance()
600 target_scm = pull_request.target_repo.scm_instance()
384 target_scm_name = target_scm.name
601 target_scm_name = target_scm.name
385 assert pull_request.is_closed()
602 assert pull_request.is_closed()
386
603
387 # check only the latest status, not the review status
604 # check only the latest status, not the review status
388 status = ChangesetStatusModel().get_status(
605 status = ChangesetStatusModel().get_status(
389 pull_request.source_repo, pull_request=pull_request)
606 pull_request.source_repo, pull_request=pull_request)
390 assert status == ChangesetStatus.STATUS_REJECTED
607 assert status == ChangesetStatus.STATUS_REJECTED
391
608
392 comment_id = response.json.get('comment_id', None)
609 comment_id = response.json.get('comment_id', None)
393 test_text = 'test'
610 test_text = 'test'
394 response = self.app.post(
611 response = self.app.post(
395 route_path(
612 route_path(
396 'pullrequest_comment_edit',
613 'pullrequest_comment_edit',
397 repo_name=target_scm_name,
614 repo_name=target_scm_name,
398 pull_request_id=pull_request_id,
615 pull_request_id=pull_request_id,
399 comment_id=comment_id,
616 comment_id=comment_id,
400 ),
617 ),
401 extra_environ=xhr_header,
618 extra_environ=xhr_header,
402 params={
619 params={
403 'csrf_token': csrf_token,
620 'csrf_token': csrf_token,
404 'text': test_text,
621 'text': test_text,
405 },
622 },
406 status=403,
623 status=403,
407 )
624 )
408 assert response.status_int == 403
625 assert response.status_int == 403
409
626
410 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
627 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
411 pull_request = pr_util.create_pull_request()
628 pull_request = pr_util.create_pull_request()
412 target_scm = pull_request.target_repo.scm_instance()
629 target_scm = pull_request.target_repo.scm_instance()
413 target_scm_name = target_scm.name
630 target_scm_name = target_scm.name
414
631
415 response = self.app.post(
632 response = self.app.post(
416 route_path(
633 route_path(
417 'pullrequest_comment_create',
634 'pullrequest_comment_create',
418 repo_name=target_scm_name,
635 repo_name=target_scm_name,
419 pull_request_id=pull_request.pull_request_id),
636 pull_request_id=pull_request.pull_request_id),
420 params={
637 params={
421 'csrf_token': csrf_token,
638 'csrf_token': csrf_token,
422 'text': 'init',
639 'text': 'init',
423 },
640 },
424 extra_environ=xhr_header,
641 extra_environ=xhr_header,
425 )
642 )
426 assert response.json
643 assert response.json
427
644
428 comment_id = response.json.get('comment_id', None)
645 comment_id = response.json.get('comment_id', None)
429 assert comment_id
646 assert comment_id
430 test_text = 'test'
647 test_text = 'test'
431 self.app.post(
648 self.app.post(
432 route_path(
649 route_path(
433 'pullrequest_comment_edit',
650 'pullrequest_comment_edit',
434 repo_name=target_scm_name,
651 repo_name=target_scm_name,
435 pull_request_id=pull_request.pull_request_id,
652 pull_request_id=pull_request.pull_request_id,
436 comment_id=comment_id,
653 comment_id=comment_id,
437 ),
654 ),
438 extra_environ=xhr_header,
655 extra_environ=xhr_header,
439 params={
656 params={
440 'csrf_token': csrf_token,
657 'csrf_token': csrf_token,
441 'text': test_text,
658 'text': test_text,
442 'version': '0',
659 'version': '0',
443 },
660 },
444
661
445 )
662 )
446 text_form_db = ChangesetComment.query().filter(
663 text_form_db = ChangesetComment.query().filter(
447 ChangesetComment.comment_id == comment_id).first().text
664 ChangesetComment.comment_id == comment_id).first().text
448 assert test_text == text_form_db
665 assert test_text == text_form_db
449
666
450 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
667 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
451 pull_request = pr_util.create_pull_request()
668 pull_request = pr_util.create_pull_request()
452 target_scm = pull_request.target_repo.scm_instance()
669 target_scm = pull_request.target_repo.scm_instance()
453 target_scm_name = target_scm.name
670 target_scm_name = target_scm.name
454
671
455 response = self.app.post(
672 response = self.app.post(
456 route_path(
673 route_path(
457 'pullrequest_comment_create',
674 'pullrequest_comment_create',
458 repo_name=target_scm_name,
675 repo_name=target_scm_name,
459 pull_request_id=pull_request.pull_request_id),
676 pull_request_id=pull_request.pull_request_id),
460 params={
677 params={
461 'csrf_token': csrf_token,
678 'csrf_token': csrf_token,
462 'text': 'init',
679 'text': 'init',
463 },
680 },
464 extra_environ=xhr_header,
681 extra_environ=xhr_header,
465 )
682 )
466 assert response.json
683 assert response.json
467
684
468 comment_id = response.json.get('comment_id', None)
685 comment_id = response.json.get('comment_id', None)
469 assert comment_id
686 assert comment_id
470 test_text = 'init'
687 test_text = 'init'
471 response = self.app.post(
688 response = self.app.post(
472 route_path(
689 route_path(
473 'pullrequest_comment_edit',
690 'pullrequest_comment_edit',
474 repo_name=target_scm_name,
691 repo_name=target_scm_name,
475 pull_request_id=pull_request.pull_request_id,
692 pull_request_id=pull_request.pull_request_id,
476 comment_id=comment_id,
693 comment_id=comment_id,
477 ),
694 ),
478 extra_environ=xhr_header,
695 extra_environ=xhr_header,
479 params={
696 params={
480 'csrf_token': csrf_token,
697 'csrf_token': csrf_token,
481 'text': test_text,
698 'text': test_text,
482 'version': '0',
699 'version': '0',
483 },
700 },
484 status=404,
701 status=404,
485
702
486 )
703 )
487 assert response.status_int == 404
704 assert response.status_int == 404
488
705
489 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
706 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
490 pull_request = pr_util.create_pull_request()
707 pull_request = pr_util.create_pull_request()
491 target_scm = pull_request.target_repo.scm_instance()
708 target_scm = pull_request.target_repo.scm_instance()
492 target_scm_name = target_scm.name
709 target_scm_name = target_scm.name
493
710
494 response = self.app.post(
711 response = self.app.post(
495 route_path(
712 route_path(
496 'pullrequest_comment_create',
713 'pullrequest_comment_create',
497 repo_name=target_scm_name,
714 repo_name=target_scm_name,
498 pull_request_id=pull_request.pull_request_id),
715 pull_request_id=pull_request.pull_request_id),
499 params={
716 params={
500 'csrf_token': csrf_token,
717 'csrf_token': csrf_token,
501 'text': 'init',
718 'text': 'init',
502 },
719 },
503 extra_environ=xhr_header,
720 extra_environ=xhr_header,
504 )
721 )
505 assert response.json
722 assert response.json
506 comment_id = response.json.get('comment_id', None)
723 comment_id = response.json.get('comment_id', None)
507 assert comment_id
724 assert comment_id
508
725
509 test_text = 'test'
726 test_text = 'test'
510 self.app.post(
727 self.app.post(
511 route_path(
728 route_path(
512 'pullrequest_comment_edit',
729 'pullrequest_comment_edit',
513 repo_name=target_scm_name,
730 repo_name=target_scm_name,
514 pull_request_id=pull_request.pull_request_id,
731 pull_request_id=pull_request.pull_request_id,
515 comment_id=comment_id,
732 comment_id=comment_id,
516 ),
733 ),
517 extra_environ=xhr_header,
734 extra_environ=xhr_header,
518 params={
735 params={
519 'csrf_token': csrf_token,
736 'csrf_token': csrf_token,
520 'text': test_text,
737 'text': test_text,
521 'version': '0',
738 'version': '0',
522 },
739 },
523
740
524 )
741 )
525 test_text_v2 = 'test_v2'
742 test_text_v2 = 'test_v2'
526 response = self.app.post(
743 response = self.app.post(
527 route_path(
744 route_path(
528 'pullrequest_comment_edit',
745 'pullrequest_comment_edit',
529 repo_name=target_scm_name,
746 repo_name=target_scm_name,
530 pull_request_id=pull_request.pull_request_id,
747 pull_request_id=pull_request.pull_request_id,
531 comment_id=comment_id,
748 comment_id=comment_id,
532 ),
749 ),
533 extra_environ=xhr_header,
750 extra_environ=xhr_header,
534 params={
751 params={
535 'csrf_token': csrf_token,
752 'csrf_token': csrf_token,
536 'text': test_text_v2,
753 'text': test_text_v2,
537 'version': '0',
754 'version': '0',
538 },
755 },
539 status=409,
756 status=409,
540 )
757 )
541 assert response.status_int == 409
758 assert response.status_int == 409
542
759
543 text_form_db = ChangesetComment.query().filter(
760 text_form_db = ChangesetComment.query().filter(
544 ChangesetComment.comment_id == comment_id).first().text
761 ChangesetComment.comment_id == comment_id).first().text
545
762
546 assert test_text == text_form_db
763 assert test_text == text_form_db
547 assert test_text_v2 != text_form_db
764 assert test_text_v2 != text_form_db
548
765
549 def test_comment_and_comment_edit_permissions_forbidden(
766 def test_comment_and_comment_edit_permissions_forbidden(
550 self, autologin_regular_user, user_regular, user_admin, pr_util,
767 self, autologin_regular_user, user_regular, user_admin, pr_util,
551 csrf_token, xhr_header):
768 csrf_token, xhr_header):
552 pull_request = pr_util.create_pull_request(
769 pull_request = pr_util.create_pull_request(
553 author=user_admin.username, enable_notifications=False)
770 author=user_admin.username, enable_notifications=False)
554 comment = CommentsModel().create(
771 comment = CommentsModel().create(
555 text='test',
772 text='test',
556 repo=pull_request.target_repo.scm_instance().name,
773 repo=pull_request.target_repo.scm_instance().name,
557 user=user_admin,
774 user=user_admin,
558 pull_request=pull_request,
775 pull_request=pull_request,
559 )
776 )
560 response = self.app.post(
777 response = self.app.post(
561 route_path(
778 route_path(
562 'pullrequest_comment_edit',
779 'pullrequest_comment_edit',
563 repo_name=pull_request.target_repo.scm_instance().name,
780 repo_name=pull_request.target_repo.scm_instance().name,
564 pull_request_id=pull_request.pull_request_id,
781 pull_request_id=pull_request.pull_request_id,
565 comment_id=comment.comment_id,
782 comment_id=comment.comment_id,
566 ),
783 ),
567 extra_environ=xhr_header,
784 extra_environ=xhr_header,
568 params={
785 params={
569 'csrf_token': csrf_token,
786 'csrf_token': csrf_token,
570 'text': 'test_text',
787 'text': 'test_text',
571 },
788 },
572 status=403,
789 status=403,
573 )
790 )
574 assert response.status_int == 403
791 assert response.status_int == 403
575
792
576 def test_create_pull_request(self, backend, csrf_token):
793 def test_create_pull_request(self, backend, csrf_token):
577 commits = [
794 commits = [
578 {'message': 'ancestor'},
795 {'message': 'ancestor'},
579 {'message': 'change'},
796 {'message': 'change'},
580 {'message': 'change2'},
797 {'message': 'change2'},
581 ]
798 ]
582 commit_ids = backend.create_master_repo(commits)
799 commit_ids = backend.create_master_repo(commits)
583 target = backend.create_repo(heads=['ancestor'])
800 target = backend.create_repo(heads=['ancestor'])
584 source = backend.create_repo(heads=['change2'])
801 source = backend.create_repo(heads=['change2'])
585
802
586 response = self.app.post(
803 response = self.app.post(
587 route_path('pullrequest_create', repo_name=source.repo_name),
804 route_path('pullrequest_create', repo_name=source.repo_name),
588 [
805 [
589 ('source_repo', source.repo_name),
806 ('source_repo', source.repo_name),
590 ('source_ref', 'branch:default:' + commit_ids['change2']),
807 ('source_ref', 'branch:default:' + commit_ids['change2']),
591 ('target_repo', target.repo_name),
808 ('target_repo', target.repo_name),
592 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
809 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
593 ('common_ancestor', commit_ids['ancestor']),
810 ('common_ancestor', commit_ids['ancestor']),
594 ('pullrequest_title', 'Title'),
811 ('pullrequest_title', 'Title'),
595 ('pullrequest_desc', 'Description'),
812 ('pullrequest_desc', 'Description'),
596 ('description_renderer', 'markdown'),
813 ('description_renderer', 'markdown'),
597 ('__start__', 'review_members:sequence'),
814 ('__start__', 'review_members:sequence'),
598 ('__start__', 'reviewer:mapping'),
815 ('__start__', 'reviewer:mapping'),
599 ('user_id', '1'),
816 ('user_id', '1'),
600 ('__start__', 'reasons:sequence'),
817 ('__start__', 'reasons:sequence'),
601 ('reason', 'Some reason'),
818 ('reason', 'Some reason'),
602 ('__end__', 'reasons:sequence'),
819 ('__end__', 'reasons:sequence'),
603 ('__start__', 'rules:sequence'),
820 ('__start__', 'rules:sequence'),
604 ('__end__', 'rules:sequence'),
821 ('__end__', 'rules:sequence'),
605 ('mandatory', 'False'),
822 ('mandatory', 'False'),
606 ('__end__', 'reviewer:mapping'),
823 ('__end__', 'reviewer:mapping'),
607 ('__end__', 'review_members:sequence'),
824 ('__end__', 'review_members:sequence'),
608 ('__start__', 'revisions:sequence'),
825 ('__start__', 'revisions:sequence'),
609 ('revisions', commit_ids['change']),
826 ('revisions', commit_ids['change']),
610 ('revisions', commit_ids['change2']),
827 ('revisions', commit_ids['change2']),
611 ('__end__', 'revisions:sequence'),
828 ('__end__', 'revisions:sequence'),
612 ('user', ''),
829 ('user', ''),
613 ('csrf_token', csrf_token),
830 ('csrf_token', csrf_token),
614 ],
831 ],
615 status=302)
832 status=302)
616
833
617 location = response.headers['Location']
834 location = response.headers['Location']
618 pull_request_id = location.rsplit('/', 1)[1]
835 pull_request_id = location.rsplit('/', 1)[1]
619 assert pull_request_id != 'new'
836 assert pull_request_id != 'new'
620 pull_request = PullRequest.get(int(pull_request_id))
837 pull_request = PullRequest.get(int(pull_request_id))
621
838
622 # check that we have now both revisions
839 # check that we have now both revisions
623 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
840 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
624 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
841 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
625 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
842 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
626 assert pull_request.target_ref == expected_target_ref
843 assert pull_request.target_ref == expected_target_ref
627
844
628 def test_reviewer_notifications(self, backend, csrf_token):
845 def test_reviewer_notifications(self, backend, csrf_token):
629 # We have to use the app.post for this test so it will create the
846 # We have to use the app.post for this test so it will create the
630 # notifications properly with the new PR
847 # notifications properly with the new PR
631 commits = [
848 commits = [
632 {'message': 'ancestor',
849 {'message': 'ancestor',
633 'added': [FileNode('file_A', content='content_of_ancestor')]},
850 'added': [FileNode('file_A', content='content_of_ancestor')]},
634 {'message': 'change',
851 {'message': 'change',
635 'added': [FileNode('file_a', content='content_of_change')]},
852 'added': [FileNode('file_a', content='content_of_change')]},
636 {'message': 'change-child'},
853 {'message': 'change-child'},
637 {'message': 'ancestor-child', 'parents': ['ancestor'],
854 {'message': 'ancestor-child', 'parents': ['ancestor'],
638 'added': [
855 'added': [
639 FileNode('file_B', content='content_of_ancestor_child')]},
856 FileNode('file_B', content='content_of_ancestor_child')]},
640 {'message': 'ancestor-child-2'},
857 {'message': 'ancestor-child-2'},
641 ]
858 ]
642 commit_ids = backend.create_master_repo(commits)
859 commit_ids = backend.create_master_repo(commits)
643 target = backend.create_repo(heads=['ancestor-child'])
860 target = backend.create_repo(heads=['ancestor-child'])
644 source = backend.create_repo(heads=['change'])
861 source = backend.create_repo(heads=['change'])
645
862
646 response = self.app.post(
863 response = self.app.post(
647 route_path('pullrequest_create', repo_name=source.repo_name),
864 route_path('pullrequest_create', repo_name=source.repo_name),
648 [
865 [
649 ('source_repo', source.repo_name),
866 ('source_repo', source.repo_name),
650 ('source_ref', 'branch:default:' + commit_ids['change']),
867 ('source_ref', 'branch:default:' + commit_ids['change']),
651 ('target_repo', target.repo_name),
868 ('target_repo', target.repo_name),
652 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
869 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
653 ('common_ancestor', commit_ids['ancestor']),
870 ('common_ancestor', commit_ids['ancestor']),
654 ('pullrequest_title', 'Title'),
871 ('pullrequest_title', 'Title'),
655 ('pullrequest_desc', 'Description'),
872 ('pullrequest_desc', 'Description'),
656 ('description_renderer', 'markdown'),
873 ('description_renderer', 'markdown'),
657 ('__start__', 'review_members:sequence'),
874 ('__start__', 'review_members:sequence'),
658 ('__start__', 'reviewer:mapping'),
875 ('__start__', 'reviewer:mapping'),
659 ('user_id', '2'),
876 ('user_id', '2'),
660 ('__start__', 'reasons:sequence'),
877 ('__start__', 'reasons:sequence'),
661 ('reason', 'Some reason'),
878 ('reason', 'Some reason'),
662 ('__end__', 'reasons:sequence'),
879 ('__end__', 'reasons:sequence'),
663 ('__start__', 'rules:sequence'),
880 ('__start__', 'rules:sequence'),
664 ('__end__', 'rules:sequence'),
881 ('__end__', 'rules:sequence'),
665 ('mandatory', 'False'),
882 ('mandatory', 'False'),
666 ('__end__', 'reviewer:mapping'),
883 ('__end__', 'reviewer:mapping'),
667 ('__end__', 'review_members:sequence'),
884 ('__end__', 'review_members:sequence'),
668 ('__start__', 'revisions:sequence'),
885 ('__start__', 'revisions:sequence'),
669 ('revisions', commit_ids['change']),
886 ('revisions', commit_ids['change']),
670 ('__end__', 'revisions:sequence'),
887 ('__end__', 'revisions:sequence'),
671 ('user', ''),
888 ('user', ''),
672 ('csrf_token', csrf_token),
889 ('csrf_token', csrf_token),
673 ],
890 ],
674 status=302)
891 status=302)
675
892
676 location = response.headers['Location']
893 location = response.headers['Location']
677
894
678 pull_request_id = location.rsplit('/', 1)[1]
895 pull_request_id = location.rsplit('/', 1)[1]
679 assert pull_request_id != 'new'
896 assert pull_request_id != 'new'
680 pull_request = PullRequest.get(int(pull_request_id))
897 pull_request = PullRequest.get(int(pull_request_id))
681
898
682 # Check that a notification was made
899 # Check that a notification was made
683 notifications = Notification.query()\
900 notifications = Notification.query()\
684 .filter(Notification.created_by == pull_request.author.user_id,
901 .filter(Notification.created_by == pull_request.author.user_id,
685 Notification.type_ == Notification.TYPE_PULL_REQUEST,
902 Notification.type_ == Notification.TYPE_PULL_REQUEST,
686 Notification.subject.contains(
903 Notification.subject.contains(
687 "requested a pull request review. !%s" % pull_request_id))
904 "requested a pull request review. !%s" % pull_request_id))
688 assert len(notifications.all()) == 1
905 assert len(notifications.all()) == 1
689
906
690 # Change reviewers and check that a notification was made
907 # Change reviewers and check that a notification was made
691 PullRequestModel().update_reviewers(
908 PullRequestModel().update_reviewers(
692 pull_request.pull_request_id, [(1, [], False, [])],
909 pull_request.pull_request_id, [(1, [], False, [])],
693 pull_request.author)
910 pull_request.author)
694 assert len(notifications.all()) == 2
911 assert len(notifications.all()) == 2
695
912
696 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
913 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
697 csrf_token):
914 csrf_token):
698 commits = [
915 commits = [
699 {'message': 'ancestor',
916 {'message': 'ancestor',
700 'added': [FileNode('file_A', content='content_of_ancestor')]},
917 'added': [FileNode('file_A', content='content_of_ancestor')]},
701 {'message': 'change',
918 {'message': 'change',
702 'added': [FileNode('file_a', content='content_of_change')]},
919 'added': [FileNode('file_a', content='content_of_change')]},
703 {'message': 'change-child'},
920 {'message': 'change-child'},
704 {'message': 'ancestor-child', 'parents': ['ancestor'],
921 {'message': 'ancestor-child', 'parents': ['ancestor'],
705 'added': [
922 'added': [
706 FileNode('file_B', content='content_of_ancestor_child')]},
923 FileNode('file_B', content='content_of_ancestor_child')]},
707 {'message': 'ancestor-child-2'},
924 {'message': 'ancestor-child-2'},
708 ]
925 ]
709 commit_ids = backend.create_master_repo(commits)
926 commit_ids = backend.create_master_repo(commits)
710 target = backend.create_repo(heads=['ancestor-child'])
927 target = backend.create_repo(heads=['ancestor-child'])
711 source = backend.create_repo(heads=['change'])
928 source = backend.create_repo(heads=['change'])
712
929
713 response = self.app.post(
930 response = self.app.post(
714 route_path('pullrequest_create', repo_name=source.repo_name),
931 route_path('pullrequest_create', repo_name=source.repo_name),
715 [
932 [
716 ('source_repo', source.repo_name),
933 ('source_repo', source.repo_name),
717 ('source_ref', 'branch:default:' + commit_ids['change']),
934 ('source_ref', 'branch:default:' + commit_ids['change']),
718 ('target_repo', target.repo_name),
935 ('target_repo', target.repo_name),
719 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
936 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
720 ('common_ancestor', commit_ids['ancestor']),
937 ('common_ancestor', commit_ids['ancestor']),
721 ('pullrequest_title', 'Title'),
938 ('pullrequest_title', 'Title'),
722 ('pullrequest_desc', 'Description'),
939 ('pullrequest_desc', 'Description'),
723 ('description_renderer', 'markdown'),
940 ('description_renderer', 'markdown'),
724 ('__start__', 'review_members:sequence'),
941 ('__start__', 'review_members:sequence'),
725 ('__start__', 'reviewer:mapping'),
942 ('__start__', 'reviewer:mapping'),
726 ('user_id', '1'),
943 ('user_id', '1'),
727 ('__start__', 'reasons:sequence'),
944 ('__start__', 'reasons:sequence'),
728 ('reason', 'Some reason'),
945 ('reason', 'Some reason'),
729 ('__end__', 'reasons:sequence'),
946 ('__end__', 'reasons:sequence'),
730 ('__start__', 'rules:sequence'),
947 ('__start__', 'rules:sequence'),
731 ('__end__', 'rules:sequence'),
948 ('__end__', 'rules:sequence'),
732 ('mandatory', 'False'),
949 ('mandatory', 'False'),
733 ('__end__', 'reviewer:mapping'),
950 ('__end__', 'reviewer:mapping'),
734 ('__end__', 'review_members:sequence'),
951 ('__end__', 'review_members:sequence'),
735 ('__start__', 'revisions:sequence'),
952 ('__start__', 'revisions:sequence'),
736 ('revisions', commit_ids['change']),
953 ('revisions', commit_ids['change']),
737 ('__end__', 'revisions:sequence'),
954 ('__end__', 'revisions:sequence'),
738 ('user', ''),
955 ('user', ''),
739 ('csrf_token', csrf_token),
956 ('csrf_token', csrf_token),
740 ],
957 ],
741 status=302)
958 status=302)
742
959
743 location = response.headers['Location']
960 location = response.headers['Location']
744
961
745 pull_request_id = location.rsplit('/', 1)[1]
962 pull_request_id = location.rsplit('/', 1)[1]
746 assert pull_request_id != 'new'
963 assert pull_request_id != 'new'
747 pull_request = PullRequest.get(int(pull_request_id))
964 pull_request = PullRequest.get(int(pull_request_id))
748
965
749 # target_ref has to point to the ancestor's commit_id in order to
966 # target_ref has to point to the ancestor's commit_id in order to
750 # show the correct diff
967 # show the correct diff
751 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
968 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
752 assert pull_request.target_ref == expected_target_ref
969 assert pull_request.target_ref == expected_target_ref
753
970
754 # Check generated diff contents
971 # Check generated diff contents
755 response = response.follow()
972 response = response.follow()
756 response.mustcontain(no=['content_of_ancestor'])
973 response.mustcontain(no=['content_of_ancestor'])
757 response.mustcontain(no=['content_of_ancestor-child'])
974 response.mustcontain(no=['content_of_ancestor-child'])
758 response.mustcontain('content_of_change')
975 response.mustcontain('content_of_change')
759
976
760 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
977 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
761 # Clear any previous calls to rcextensions
978 # Clear any previous calls to rcextensions
762 rhodecode.EXTENSIONS.calls.clear()
979 rhodecode.EXTENSIONS.calls.clear()
763
980
764 pull_request = pr_util.create_pull_request(
981 pull_request = pr_util.create_pull_request(
765 approved=True, mergeable=True)
982 approved=True, mergeable=True)
766 pull_request_id = pull_request.pull_request_id
983 pull_request_id = pull_request.pull_request_id
767 repo_name = pull_request.target_repo.scm_instance().name,
984 repo_name = pull_request.target_repo.scm_instance().name,
768
985
769 url = route_path('pullrequest_merge',
986 url = route_path('pullrequest_merge',
770 repo_name=str(repo_name[0]),
987 repo_name=str(repo_name[0]),
771 pull_request_id=pull_request_id)
988 pull_request_id=pull_request_id)
772 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
989 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
773
990
774 pull_request = PullRequest.get(pull_request_id)
991 pull_request = PullRequest.get(pull_request_id)
775
992
776 assert response.status_int == 200
993 assert response.status_int == 200
777 assert pull_request.is_closed()
994 assert pull_request.is_closed()
778 assert_pull_request_status(
995 assert_pull_request_status(
779 pull_request, ChangesetStatus.STATUS_APPROVED)
996 pull_request, ChangesetStatus.STATUS_APPROVED)
780
997
781 # Check the relevant log entries were added
998 # Check the relevant log entries were added
782 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
999 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
783 actions = [log.action for log in user_logs]
1000 actions = [log.action for log in user_logs]
784 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1001 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
785 expected_actions = [
1002 expected_actions = [
786 u'repo.pull_request.close',
1003 u'repo.pull_request.close',
787 u'repo.pull_request.merge',
1004 u'repo.pull_request.merge',
788 u'repo.pull_request.comment.create'
1005 u'repo.pull_request.comment.create'
789 ]
1006 ]
790 assert actions == expected_actions
1007 assert actions == expected_actions
791
1008
792 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1009 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
793 actions = [log for log in user_logs]
1010 actions = [log for log in user_logs]
794 assert actions[-1].action == 'user.push'
1011 assert actions[-1].action == 'user.push'
795 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1012 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
796
1013
797 # Check post_push rcextension was really executed
1014 # Check post_push rcextension was really executed
798 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1015 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
799 assert len(push_calls) == 1
1016 assert len(push_calls) == 1
800 unused_last_call_args, last_call_kwargs = push_calls[0]
1017 unused_last_call_args, last_call_kwargs = push_calls[0]
801 assert last_call_kwargs['action'] == 'push'
1018 assert last_call_kwargs['action'] == 'push'
802 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1019 assert last_call_kwargs['commit_ids'] == pr_commit_ids
803
1020
804 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1021 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
805 pull_request = pr_util.create_pull_request(mergeable=False)
1022 pull_request = pr_util.create_pull_request(mergeable=False)
806 pull_request_id = pull_request.pull_request_id
1023 pull_request_id = pull_request.pull_request_id
807 pull_request = PullRequest.get(pull_request_id)
1024 pull_request = PullRequest.get(pull_request_id)
808
1025
809 response = self.app.post(
1026 response = self.app.post(
810 route_path('pullrequest_merge',
1027 route_path('pullrequest_merge',
811 repo_name=pull_request.target_repo.scm_instance().name,
1028 repo_name=pull_request.target_repo.scm_instance().name,
812 pull_request_id=pull_request.pull_request_id),
1029 pull_request_id=pull_request.pull_request_id),
813 params={'csrf_token': csrf_token}).follow()
1030 params={'csrf_token': csrf_token}).follow()
814
1031
815 assert response.status_int == 200
1032 assert response.status_int == 200
816 response.mustcontain(
1033 response.mustcontain(
817 'Merge is not currently possible because of below failed checks.')
1034 'Merge is not currently possible because of below failed checks.')
818 response.mustcontain('Server-side pull request merging is disabled.')
1035 response.mustcontain('Server-side pull request merging is disabled.')
819
1036
820 @pytest.mark.skip_backends('svn')
1037 @pytest.mark.skip_backends('svn')
821 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1038 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
822 pull_request = pr_util.create_pull_request(mergeable=True)
1039 pull_request = pr_util.create_pull_request(mergeable=True)
823 pull_request_id = pull_request.pull_request_id
1040 pull_request_id = pull_request.pull_request_id
824 repo_name = pull_request.target_repo.scm_instance().name
1041 repo_name = pull_request.target_repo.scm_instance().name
825
1042
826 response = self.app.post(
1043 response = self.app.post(
827 route_path('pullrequest_merge',
1044 route_path('pullrequest_merge',
828 repo_name=repo_name, pull_request_id=pull_request_id),
1045 repo_name=repo_name, pull_request_id=pull_request_id),
829 params={'csrf_token': csrf_token}).follow()
1046 params={'csrf_token': csrf_token}).follow()
830
1047
831 assert response.status_int == 200
1048 assert response.status_int == 200
832
1049
833 response.mustcontain(
1050 response.mustcontain(
834 'Merge is not currently possible because of below failed checks.')
1051 'Merge is not currently possible because of below failed checks.')
835 response.mustcontain('Pull request reviewer approval is pending.')
1052 response.mustcontain('Pull request reviewer approval is pending.')
836
1053
837 def test_merge_pull_request_renders_failure_reason(
1054 def test_merge_pull_request_renders_failure_reason(
838 self, user_regular, csrf_token, pr_util):
1055 self, user_regular, csrf_token, pr_util):
839 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1056 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
840 pull_request_id = pull_request.pull_request_id
1057 pull_request_id = pull_request.pull_request_id
841 repo_name = pull_request.target_repo.scm_instance().name
1058 repo_name = pull_request.target_repo.scm_instance().name
842
1059
843 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1060 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
844 MergeFailureReason.PUSH_FAILED,
1061 MergeFailureReason.PUSH_FAILED,
845 metadata={'target': 'shadow repo',
1062 metadata={'target': 'shadow repo',
846 'merge_commit': 'xxx'})
1063 'merge_commit': 'xxx'})
847 model_patcher = mock.patch.multiple(
1064 model_patcher = mock.patch.multiple(
848 PullRequestModel,
1065 PullRequestModel,
849 merge_repo=mock.Mock(return_value=merge_resp),
1066 merge_repo=mock.Mock(return_value=merge_resp),
850 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1067 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
851
1068
852 with model_patcher:
1069 with model_patcher:
853 response = self.app.post(
1070 response = self.app.post(
854 route_path('pullrequest_merge',
1071 route_path('pullrequest_merge',
855 repo_name=repo_name,
1072 repo_name=repo_name,
856 pull_request_id=pull_request_id),
1073 pull_request_id=pull_request_id),
857 params={'csrf_token': csrf_token}, status=302)
1074 params={'csrf_token': csrf_token}, status=302)
858
1075
859 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1076 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
860 metadata={'target': 'shadow repo',
1077 metadata={'target': 'shadow repo',
861 'merge_commit': 'xxx'})
1078 'merge_commit': 'xxx'})
862 assert_session_flash(response, merge_resp.merge_status_message)
1079 assert_session_flash(response, merge_resp.merge_status_message)
863
1080
864 def test_update_source_revision(self, backend, csrf_token):
1081 def test_update_source_revision(self, backend, csrf_token):
865 commits = [
1082 commits = [
866 {'message': 'ancestor'},
1083 {'message': 'ancestor'},
867 {'message': 'change'},
1084 {'message': 'change'},
868 {'message': 'change-2'},
1085 {'message': 'change-2'},
869 ]
1086 ]
870 commit_ids = backend.create_master_repo(commits)
1087 commit_ids = backend.create_master_repo(commits)
871 target = backend.create_repo(heads=['ancestor'])
1088 target = backend.create_repo(heads=['ancestor'])
872 source = backend.create_repo(heads=['change'])
1089 source = backend.create_repo(heads=['change'])
873
1090
874 # create pr from a in source to A in target
1091 # create pr from a in source to A in target
875 pull_request = PullRequest()
1092 pull_request = PullRequest()
876
1093
877 pull_request.source_repo = source
1094 pull_request.source_repo = source
878 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1095 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
879 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1096 branch=backend.default_branch_name, commit_id=commit_ids['change'])
880
1097
881 pull_request.target_repo = target
1098 pull_request.target_repo = target
882 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1099 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
883 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1100 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
884
1101
885 pull_request.revisions = [commit_ids['change']]
1102 pull_request.revisions = [commit_ids['change']]
886 pull_request.title = u"Test"
1103 pull_request.title = u"Test"
887 pull_request.description = u"Description"
1104 pull_request.description = u"Description"
888 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1105 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
889 pull_request.pull_request_state = PullRequest.STATE_CREATED
1106 pull_request.pull_request_state = PullRequest.STATE_CREATED
890 Session().add(pull_request)
1107 Session().add(pull_request)
891 Session().commit()
1108 Session().commit()
892 pull_request_id = pull_request.pull_request_id
1109 pull_request_id = pull_request.pull_request_id
893
1110
894 # source has ancestor - change - change-2
1111 # source has ancestor - change - change-2
895 backend.pull_heads(source, heads=['change-2'])
1112 backend.pull_heads(source, heads=['change-2'])
896
1113
897 # update PR
1114 # update PR
898 self.app.post(
1115 self.app.post(
899 route_path('pullrequest_update',
1116 route_path('pullrequest_update',
900 repo_name=target.repo_name, pull_request_id=pull_request_id),
1117 repo_name=target.repo_name, pull_request_id=pull_request_id),
901 params={'update_commits': 'true', 'csrf_token': csrf_token})
1118 params={'update_commits': 'true', 'csrf_token': csrf_token})
902
1119
903 response = self.app.get(
1120 response = self.app.get(
904 route_path('pullrequest_show',
1121 route_path('pullrequest_show',
905 repo_name=target.repo_name,
1122 repo_name=target.repo_name,
906 pull_request_id=pull_request.pull_request_id))
1123 pull_request_id=pull_request.pull_request_id))
907
1124
908 assert response.status_int == 200
1125 assert response.status_int == 200
909 response.mustcontain('Pull request updated to')
1126 response.mustcontain('Pull request updated to')
910 response.mustcontain('with 1 added, 0 removed commits.')
1127 response.mustcontain('with 1 added, 0 removed commits.')
911
1128
912 # check that we have now both revisions
1129 # check that we have now both revisions
913 pull_request = PullRequest.get(pull_request_id)
1130 pull_request = PullRequest.get(pull_request_id)
914 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1131 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
915
1132
916 def test_update_target_revision(self, backend, csrf_token):
1133 def test_update_target_revision(self, backend, csrf_token):
917 commits = [
1134 commits = [
918 {'message': 'ancestor'},
1135 {'message': 'ancestor'},
919 {'message': 'change'},
1136 {'message': 'change'},
920 {'message': 'ancestor-new', 'parents': ['ancestor']},
1137 {'message': 'ancestor-new', 'parents': ['ancestor']},
921 {'message': 'change-rebased'},
1138 {'message': 'change-rebased'},
922 ]
1139 ]
923 commit_ids = backend.create_master_repo(commits)
1140 commit_ids = backend.create_master_repo(commits)
924 target = backend.create_repo(heads=['ancestor'])
1141 target = backend.create_repo(heads=['ancestor'])
925 source = backend.create_repo(heads=['change'])
1142 source = backend.create_repo(heads=['change'])
926
1143
927 # create pr from a in source to A in target
1144 # create pr from a in source to A in target
928 pull_request = PullRequest()
1145 pull_request = PullRequest()
929
1146
930 pull_request.source_repo = source
1147 pull_request.source_repo = source
931 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1148 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
932 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1149 branch=backend.default_branch_name, commit_id=commit_ids['change'])
933
1150
934 pull_request.target_repo = target
1151 pull_request.target_repo = target
935 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1152 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
936 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1153 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
937
1154
938 pull_request.revisions = [commit_ids['change']]
1155 pull_request.revisions = [commit_ids['change']]
939 pull_request.title = u"Test"
1156 pull_request.title = u"Test"
940 pull_request.description = u"Description"
1157 pull_request.description = u"Description"
941 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1158 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
942 pull_request.pull_request_state = PullRequest.STATE_CREATED
1159 pull_request.pull_request_state = PullRequest.STATE_CREATED
943
1160
944 Session().add(pull_request)
1161 Session().add(pull_request)
945 Session().commit()
1162 Session().commit()
946 pull_request_id = pull_request.pull_request_id
1163 pull_request_id = pull_request.pull_request_id
947
1164
948 # target has ancestor - ancestor-new
1165 # target has ancestor - ancestor-new
949 # source has ancestor - ancestor-new - change-rebased
1166 # source has ancestor - ancestor-new - change-rebased
950 backend.pull_heads(target, heads=['ancestor-new'])
1167 backend.pull_heads(target, heads=['ancestor-new'])
951 backend.pull_heads(source, heads=['change-rebased'])
1168 backend.pull_heads(source, heads=['change-rebased'])
952
1169
953 # update PR
1170 # update PR
954 url = route_path('pullrequest_update',
1171 url = route_path('pullrequest_update',
955 repo_name=target.repo_name,
1172 repo_name=target.repo_name,
956 pull_request_id=pull_request_id)
1173 pull_request_id=pull_request_id)
957 self.app.post(url,
1174 self.app.post(url,
958 params={'update_commits': 'true', 'csrf_token': csrf_token},
1175 params={'update_commits': 'true', 'csrf_token': csrf_token},
959 status=200)
1176 status=200)
960
1177
961 # check that we have now both revisions
1178 # check that we have now both revisions
962 pull_request = PullRequest.get(pull_request_id)
1179 pull_request = PullRequest.get(pull_request_id)
963 assert pull_request.revisions == [commit_ids['change-rebased']]
1180 assert pull_request.revisions == [commit_ids['change-rebased']]
964 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1181 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
965 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1182 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
966
1183
967 response = self.app.get(
1184 response = self.app.get(
968 route_path('pullrequest_show',
1185 route_path('pullrequest_show',
969 repo_name=target.repo_name,
1186 repo_name=target.repo_name,
970 pull_request_id=pull_request.pull_request_id))
1187 pull_request_id=pull_request.pull_request_id))
971 assert response.status_int == 200
1188 assert response.status_int == 200
972 response.mustcontain('Pull request updated to')
1189 response.mustcontain('Pull request updated to')
973 response.mustcontain('with 1 added, 1 removed commits.')
1190 response.mustcontain('with 1 added, 1 removed commits.')
974
1191
975 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1192 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
976 backend = backend_git
1193 backend = backend_git
977 commits = [
1194 commits = [
978 {'message': 'master-commit-1'},
1195 {'message': 'master-commit-1'},
979 {'message': 'master-commit-2-change-1'},
1196 {'message': 'master-commit-2-change-1'},
980 {'message': 'master-commit-3-change-2'},
1197 {'message': 'master-commit-3-change-2'},
981
1198
982 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1199 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
983 {'message': 'feat-commit-2'},
1200 {'message': 'feat-commit-2'},
984 ]
1201 ]
985 commit_ids = backend.create_master_repo(commits)
1202 commit_ids = backend.create_master_repo(commits)
986 target = backend.create_repo(heads=['master-commit-3-change-2'])
1203 target = backend.create_repo(heads=['master-commit-3-change-2'])
987 source = backend.create_repo(heads=['feat-commit-2'])
1204 source = backend.create_repo(heads=['feat-commit-2'])
988
1205
989 # create pr from a in source to A in target
1206 # create pr from a in source to A in target
990 pull_request = PullRequest()
1207 pull_request = PullRequest()
991 pull_request.source_repo = source
1208 pull_request.source_repo = source
992
1209
993 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1210 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
994 branch=backend.default_branch_name,
1211 branch=backend.default_branch_name,
995 commit_id=commit_ids['master-commit-3-change-2'])
1212 commit_id=commit_ids['master-commit-3-change-2'])
996
1213
997 pull_request.target_repo = target
1214 pull_request.target_repo = target
998 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1215 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
999 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1216 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1000
1217
1001 pull_request.revisions = [
1218 pull_request.revisions = [
1002 commit_ids['feat-commit-1'],
1219 commit_ids['feat-commit-1'],
1003 commit_ids['feat-commit-2']
1220 commit_ids['feat-commit-2']
1004 ]
1221 ]
1005 pull_request.title = u"Test"
1222 pull_request.title = u"Test"
1006 pull_request.description = u"Description"
1223 pull_request.description = u"Description"
1007 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1224 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1008 pull_request.pull_request_state = PullRequest.STATE_CREATED
1225 pull_request.pull_request_state = PullRequest.STATE_CREATED
1009 Session().add(pull_request)
1226 Session().add(pull_request)
1010 Session().commit()
1227 Session().commit()
1011 pull_request_id = pull_request.pull_request_id
1228 pull_request_id = pull_request.pull_request_id
1012
1229
1013 # PR is created, now we simulate a force-push into target,
1230 # PR is created, now we simulate a force-push into target,
1014 # that drops a 2 last commits
1231 # that drops a 2 last commits
1015 vcsrepo = target.scm_instance()
1232 vcsrepo = target.scm_instance()
1016 vcsrepo.config.clear_section('hooks')
1233 vcsrepo.config.clear_section('hooks')
1017 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1234 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1018
1235
1019 # update PR
1236 # update PR
1020 url = route_path('pullrequest_update',
1237 url = route_path('pullrequest_update',
1021 repo_name=target.repo_name,
1238 repo_name=target.repo_name,
1022 pull_request_id=pull_request_id)
1239 pull_request_id=pull_request_id)
1023 self.app.post(url,
1240 self.app.post(url,
1024 params={'update_commits': 'true', 'csrf_token': csrf_token},
1241 params={'update_commits': 'true', 'csrf_token': csrf_token},
1025 status=200)
1242 status=200)
1026
1243
1027 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
1244 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
1028 assert response.status_int == 200
1245 assert response.status_int == 200
1029 response.mustcontain('Pull request updated to')
1246 response.mustcontain('Pull request updated to')
1030 response.mustcontain('with 0 added, 0 removed commits.')
1247 response.mustcontain('with 0 added, 0 removed commits.')
1031
1248
1032 def test_update_of_ancestor_reference(self, backend, csrf_token):
1249 def test_update_of_ancestor_reference(self, backend, csrf_token):
1033 commits = [
1250 commits = [
1034 {'message': 'ancestor'},
1251 {'message': 'ancestor'},
1035 {'message': 'change'},
1252 {'message': 'change'},
1036 {'message': 'change-2'},
1253 {'message': 'change-2'},
1037 {'message': 'ancestor-new', 'parents': ['ancestor']},
1254 {'message': 'ancestor-new', 'parents': ['ancestor']},
1038 {'message': 'change-rebased'},
1255 {'message': 'change-rebased'},
1039 ]
1256 ]
1040 commit_ids = backend.create_master_repo(commits)
1257 commit_ids = backend.create_master_repo(commits)
1041 target = backend.create_repo(heads=['ancestor'])
1258 target = backend.create_repo(heads=['ancestor'])
1042 source = backend.create_repo(heads=['change'])
1259 source = backend.create_repo(heads=['change'])
1043
1260
1044 # create pr from a in source to A in target
1261 # create pr from a in source to A in target
1045 pull_request = PullRequest()
1262 pull_request = PullRequest()
1046 pull_request.source_repo = source
1263 pull_request.source_repo = source
1047
1264
1048 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1265 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1049 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1266 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1050 pull_request.target_repo = target
1267 pull_request.target_repo = target
1051 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1268 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1052 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1269 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1053 pull_request.revisions = [commit_ids['change']]
1270 pull_request.revisions = [commit_ids['change']]
1054 pull_request.title = u"Test"
1271 pull_request.title = u"Test"
1055 pull_request.description = u"Description"
1272 pull_request.description = u"Description"
1056 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1273 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1057 pull_request.pull_request_state = PullRequest.STATE_CREATED
1274 pull_request.pull_request_state = PullRequest.STATE_CREATED
1058 Session().add(pull_request)
1275 Session().add(pull_request)
1059 Session().commit()
1276 Session().commit()
1060 pull_request_id = pull_request.pull_request_id
1277 pull_request_id = pull_request.pull_request_id
1061
1278
1062 # target has ancestor - ancestor-new
1279 # target has ancestor - ancestor-new
1063 # source has ancestor - ancestor-new - change-rebased
1280 # source has ancestor - ancestor-new - change-rebased
1064 backend.pull_heads(target, heads=['ancestor-new'])
1281 backend.pull_heads(target, heads=['ancestor-new'])
1065 backend.pull_heads(source, heads=['change-rebased'])
1282 backend.pull_heads(source, heads=['change-rebased'])
1066
1283
1067 # update PR
1284 # update PR
1068 self.app.post(
1285 self.app.post(
1069 route_path('pullrequest_update',
1286 route_path('pullrequest_update',
1070 repo_name=target.repo_name, pull_request_id=pull_request_id),
1287 repo_name=target.repo_name, pull_request_id=pull_request_id),
1071 params={'update_commits': 'true', 'csrf_token': csrf_token},
1288 params={'update_commits': 'true', 'csrf_token': csrf_token},
1072 status=200)
1289 status=200)
1073
1290
1074 # Expect the target reference to be updated correctly
1291 # Expect the target reference to be updated correctly
1075 pull_request = PullRequest.get(pull_request_id)
1292 pull_request = PullRequest.get(pull_request_id)
1076 assert pull_request.revisions == [commit_ids['change-rebased']]
1293 assert pull_request.revisions == [commit_ids['change-rebased']]
1077 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1294 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1078 branch=backend.default_branch_name,
1295 branch=backend.default_branch_name,
1079 commit_id=commit_ids['ancestor-new'])
1296 commit_id=commit_ids['ancestor-new'])
1080 assert pull_request.target_ref == expected_target_ref
1297 assert pull_request.target_ref == expected_target_ref
1081
1298
1082 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1299 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1083 branch_name = 'development'
1300 branch_name = 'development'
1084 commits = [
1301 commits = [
1085 {'message': 'initial-commit'},
1302 {'message': 'initial-commit'},
1086 {'message': 'old-feature'},
1303 {'message': 'old-feature'},
1087 {'message': 'new-feature', 'branch': branch_name},
1304 {'message': 'new-feature', 'branch': branch_name},
1088 ]
1305 ]
1089 repo = backend_git.create_repo(commits)
1306 repo = backend_git.create_repo(commits)
1090 repo_name = repo.repo_name
1307 repo_name = repo.repo_name
1091 commit_ids = backend_git.commit_ids
1308 commit_ids = backend_git.commit_ids
1092
1309
1093 pull_request = PullRequest()
1310 pull_request = PullRequest()
1094 pull_request.source_repo = repo
1311 pull_request.source_repo = repo
1095 pull_request.target_repo = repo
1312 pull_request.target_repo = repo
1096 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1313 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1097 branch=branch_name, commit_id=commit_ids['new-feature'])
1314 branch=branch_name, commit_id=commit_ids['new-feature'])
1098 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1315 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1099 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1316 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1100 pull_request.revisions = [commit_ids['new-feature']]
1317 pull_request.revisions = [commit_ids['new-feature']]
1101 pull_request.title = u"Test"
1318 pull_request.title = u"Test"
1102 pull_request.description = u"Description"
1319 pull_request.description = u"Description"
1103 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1320 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1104 pull_request.pull_request_state = PullRequest.STATE_CREATED
1321 pull_request.pull_request_state = PullRequest.STATE_CREATED
1105 Session().add(pull_request)
1322 Session().add(pull_request)
1106 Session().commit()
1323 Session().commit()
1107
1324
1108 pull_request_id = pull_request.pull_request_id
1325 pull_request_id = pull_request.pull_request_id
1109
1326
1110 vcs = repo.scm_instance()
1327 vcs = repo.scm_instance()
1111 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1328 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1112 # NOTE(marcink): run GC to ensure the commits are gone
1329 # NOTE(marcink): run GC to ensure the commits are gone
1113 vcs.run_gc()
1330 vcs.run_gc()
1114
1331
1115 response = self.app.get(route_path(
1332 response = self.app.get(route_path(
1116 'pullrequest_show',
1333 'pullrequest_show',
1117 repo_name=repo_name,
1334 repo_name=repo_name,
1118 pull_request_id=pull_request_id))
1335 pull_request_id=pull_request_id))
1119
1336
1120 assert response.status_int == 200
1337 assert response.status_int == 200
1121
1338
1122 response.assert_response().element_contains(
1339 response.assert_response().element_contains(
1123 '#changeset_compare_view_content .alert strong',
1340 '#changeset_compare_view_content .alert strong',
1124 'Missing commits')
1341 'Missing commits')
1125 response.assert_response().element_contains(
1342 response.assert_response().element_contains(
1126 '#changeset_compare_view_content .alert',
1343 '#changeset_compare_view_content .alert',
1127 'This pull request cannot be displayed, because one or more'
1344 'This pull request cannot be displayed, because one or more'
1128 ' commits no longer exist in the source repository.')
1345 ' commits no longer exist in the source repository.')
1129
1346
1130 def test_strip_commits_from_pull_request(
1347 def test_strip_commits_from_pull_request(
1131 self, backend, pr_util, csrf_token):
1348 self, backend, pr_util, csrf_token):
1132 commits = [
1349 commits = [
1133 {'message': 'initial-commit'},
1350 {'message': 'initial-commit'},
1134 {'message': 'old-feature'},
1351 {'message': 'old-feature'},
1135 {'message': 'new-feature', 'parents': ['initial-commit']},
1352 {'message': 'new-feature', 'parents': ['initial-commit']},
1136 ]
1353 ]
1137 pull_request = pr_util.create_pull_request(
1354 pull_request = pr_util.create_pull_request(
1138 commits, target_head='initial-commit', source_head='new-feature',
1355 commits, target_head='initial-commit', source_head='new-feature',
1139 revisions=['new-feature'])
1356 revisions=['new-feature'])
1140
1357
1141 vcs = pr_util.source_repository.scm_instance()
1358 vcs = pr_util.source_repository.scm_instance()
1142 if backend.alias == 'git':
1359 if backend.alias == 'git':
1143 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1360 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1144 else:
1361 else:
1145 vcs.strip(pr_util.commit_ids['new-feature'])
1362 vcs.strip(pr_util.commit_ids['new-feature'])
1146
1363
1147 response = self.app.get(route_path(
1364 response = self.app.get(route_path(
1148 'pullrequest_show',
1365 'pullrequest_show',
1149 repo_name=pr_util.target_repository.repo_name,
1366 repo_name=pr_util.target_repository.repo_name,
1150 pull_request_id=pull_request.pull_request_id))
1367 pull_request_id=pull_request.pull_request_id))
1151
1368
1152 assert response.status_int == 200
1369 assert response.status_int == 200
1153
1370
1154 response.assert_response().element_contains(
1371 response.assert_response().element_contains(
1155 '#changeset_compare_view_content .alert strong',
1372 '#changeset_compare_view_content .alert strong',
1156 'Missing commits')
1373 'Missing commits')
1157 response.assert_response().element_contains(
1374 response.assert_response().element_contains(
1158 '#changeset_compare_view_content .alert',
1375 '#changeset_compare_view_content .alert',
1159 'This pull request cannot be displayed, because one or more'
1376 'This pull request cannot be displayed, because one or more'
1160 ' commits no longer exist in the source repository.')
1377 ' commits no longer exist in the source repository.')
1161 response.assert_response().element_contains(
1378 response.assert_response().element_contains(
1162 '#update_commits',
1379 '#update_commits',
1163 'Update commits')
1380 'Update commits')
1164
1381
1165 def test_strip_commits_and_update(
1382 def test_strip_commits_and_update(
1166 self, backend, pr_util, csrf_token):
1383 self, backend, pr_util, csrf_token):
1167 commits = [
1384 commits = [
1168 {'message': 'initial-commit'},
1385 {'message': 'initial-commit'},
1169 {'message': 'old-feature'},
1386 {'message': 'old-feature'},
1170 {'message': 'new-feature', 'parents': ['old-feature']},
1387 {'message': 'new-feature', 'parents': ['old-feature']},
1171 ]
1388 ]
1172 pull_request = pr_util.create_pull_request(
1389 pull_request = pr_util.create_pull_request(
1173 commits, target_head='old-feature', source_head='new-feature',
1390 commits, target_head='old-feature', source_head='new-feature',
1174 revisions=['new-feature'], mergeable=True)
1391 revisions=['new-feature'], mergeable=True)
1175
1392
1176 vcs = pr_util.source_repository.scm_instance()
1393 vcs = pr_util.source_repository.scm_instance()
1177 if backend.alias == 'git':
1394 if backend.alias == 'git':
1178 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1395 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1179 else:
1396 else:
1180 vcs.strip(pr_util.commit_ids['new-feature'])
1397 vcs.strip(pr_util.commit_ids['new-feature'])
1181
1398
1182 url = route_path('pullrequest_update',
1399 url = route_path('pullrequest_update',
1183 repo_name=pull_request.target_repo.repo_name,
1400 repo_name=pull_request.target_repo.repo_name,
1184 pull_request_id=pull_request.pull_request_id)
1401 pull_request_id=pull_request.pull_request_id)
1185 response = self.app.post(url,
1402 response = self.app.post(url,
1186 params={'update_commits': 'true',
1403 params={'update_commits': 'true',
1187 'csrf_token': csrf_token})
1404 'csrf_token': csrf_token})
1188
1405
1189 assert response.status_int == 200
1406 assert response.status_int == 200
1190 assert response.body == '{"response": true, "redirect_url": null}'
1407 assert response.body == '{"response": true, "redirect_url": null}'
1191
1408
1192 # Make sure that after update, it won't raise 500 errors
1409 # Make sure that after update, it won't raise 500 errors
1193 response = self.app.get(route_path(
1410 response = self.app.get(route_path(
1194 'pullrequest_show',
1411 'pullrequest_show',
1195 repo_name=pr_util.target_repository.repo_name,
1412 repo_name=pr_util.target_repository.repo_name,
1196 pull_request_id=pull_request.pull_request_id))
1413 pull_request_id=pull_request.pull_request_id))
1197
1414
1198 assert response.status_int == 200
1415 assert response.status_int == 200
1199 response.assert_response().element_contains(
1416 response.assert_response().element_contains(
1200 '#changeset_compare_view_content .alert strong',
1417 '#changeset_compare_view_content .alert strong',
1201 'Missing commits')
1418 'Missing commits')
1202
1419
1203 def test_branch_is_a_link(self, pr_util):
1420 def test_branch_is_a_link(self, pr_util):
1204 pull_request = pr_util.create_pull_request()
1421 pull_request = pr_util.create_pull_request()
1205 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1422 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1206 pull_request.target_ref = 'branch:target:abcdef1234567890'
1423 pull_request.target_ref = 'branch:target:abcdef1234567890'
1207 Session().add(pull_request)
1424 Session().add(pull_request)
1208 Session().commit()
1425 Session().commit()
1209
1426
1210 response = self.app.get(route_path(
1427 response = self.app.get(route_path(
1211 'pullrequest_show',
1428 'pullrequest_show',
1212 repo_name=pull_request.target_repo.scm_instance().name,
1429 repo_name=pull_request.target_repo.scm_instance().name,
1213 pull_request_id=pull_request.pull_request_id))
1430 pull_request_id=pull_request.pull_request_id))
1214 assert response.status_int == 200
1431 assert response.status_int == 200
1215
1432
1216 source = response.assert_response().get_element('.pr-source-info')
1433 source = response.assert_response().get_element('.pr-source-info')
1217 source_parent = source.getparent()
1434 source_parent = source.getparent()
1218 assert len(source_parent) == 1
1435 assert len(source_parent) == 1
1219
1436
1220 target = response.assert_response().get_element('.pr-target-info')
1437 target = response.assert_response().get_element('.pr-target-info')
1221 target_parent = target.getparent()
1438 target_parent = target.getparent()
1222 assert len(target_parent) == 1
1439 assert len(target_parent) == 1
1223
1440
1224 expected_origin_link = route_path(
1441 expected_origin_link = route_path(
1225 'repo_commits',
1442 'repo_commits',
1226 repo_name=pull_request.source_repo.scm_instance().name,
1443 repo_name=pull_request.source_repo.scm_instance().name,
1227 params=dict(branch='origin'))
1444 params=dict(branch='origin'))
1228 expected_target_link = route_path(
1445 expected_target_link = route_path(
1229 'repo_commits',
1446 'repo_commits',
1230 repo_name=pull_request.target_repo.scm_instance().name,
1447 repo_name=pull_request.target_repo.scm_instance().name,
1231 params=dict(branch='target'))
1448 params=dict(branch='target'))
1232 assert source_parent.attrib['href'] == expected_origin_link
1449 assert source_parent.attrib['href'] == expected_origin_link
1233 assert target_parent.attrib['href'] == expected_target_link
1450 assert target_parent.attrib['href'] == expected_target_link
1234
1451
1235 def test_bookmark_is_not_a_link(self, pr_util):
1452 def test_bookmark_is_not_a_link(self, pr_util):
1236 pull_request = pr_util.create_pull_request()
1453 pull_request = pr_util.create_pull_request()
1237 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1454 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1238 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1455 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1239 Session().add(pull_request)
1456 Session().add(pull_request)
1240 Session().commit()
1457 Session().commit()
1241
1458
1242 response = self.app.get(route_path(
1459 response = self.app.get(route_path(
1243 'pullrequest_show',
1460 'pullrequest_show',
1244 repo_name=pull_request.target_repo.scm_instance().name,
1461 repo_name=pull_request.target_repo.scm_instance().name,
1245 pull_request_id=pull_request.pull_request_id))
1462 pull_request_id=pull_request.pull_request_id))
1246 assert response.status_int == 200
1463 assert response.status_int == 200
1247
1464
1248 source = response.assert_response().get_element('.pr-source-info')
1465 source = response.assert_response().get_element('.pr-source-info')
1249 assert source.text.strip() == 'bookmark:origin'
1466 assert source.text.strip() == 'bookmark:origin'
1250 assert source.getparent().attrib.get('href') is None
1467 assert source.getparent().attrib.get('href') is None
1251
1468
1252 target = response.assert_response().get_element('.pr-target-info')
1469 target = response.assert_response().get_element('.pr-target-info')
1253 assert target.text.strip() == 'bookmark:target'
1470 assert target.text.strip() == 'bookmark:target'
1254 assert target.getparent().attrib.get('href') is None
1471 assert target.getparent().attrib.get('href') is None
1255
1472
1256 def test_tag_is_not_a_link(self, pr_util):
1473 def test_tag_is_not_a_link(self, pr_util):
1257 pull_request = pr_util.create_pull_request()
1474 pull_request = pr_util.create_pull_request()
1258 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1475 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1259 pull_request.target_ref = 'tag:target:abcdef1234567890'
1476 pull_request.target_ref = 'tag:target:abcdef1234567890'
1260 Session().add(pull_request)
1477 Session().add(pull_request)
1261 Session().commit()
1478 Session().commit()
1262
1479
1263 response = self.app.get(route_path(
1480 response = self.app.get(route_path(
1264 'pullrequest_show',
1481 'pullrequest_show',
1265 repo_name=pull_request.target_repo.scm_instance().name,
1482 repo_name=pull_request.target_repo.scm_instance().name,
1266 pull_request_id=pull_request.pull_request_id))
1483 pull_request_id=pull_request.pull_request_id))
1267 assert response.status_int == 200
1484 assert response.status_int == 200
1268
1485
1269 source = response.assert_response().get_element('.pr-source-info')
1486 source = response.assert_response().get_element('.pr-source-info')
1270 assert source.text.strip() == 'tag:origin'
1487 assert source.text.strip() == 'tag:origin'
1271 assert source.getparent().attrib.get('href') is None
1488 assert source.getparent().attrib.get('href') is None
1272
1489
1273 target = response.assert_response().get_element('.pr-target-info')
1490 target = response.assert_response().get_element('.pr-target-info')
1274 assert target.text.strip() == 'tag:target'
1491 assert target.text.strip() == 'tag:target'
1275 assert target.getparent().attrib.get('href') is None
1492 assert target.getparent().attrib.get('href') is None
1276
1493
1277 @pytest.mark.parametrize('mergeable', [True, False])
1494 @pytest.mark.parametrize('mergeable', [True, False])
1278 def test_shadow_repository_link(
1495 def test_shadow_repository_link(
1279 self, mergeable, pr_util, http_host_only_stub):
1496 self, mergeable, pr_util, http_host_only_stub):
1280 """
1497 """
1281 Check that the pull request summary page displays a link to the shadow
1498 Check that the pull request summary page displays a link to the shadow
1282 repository if the pull request is mergeable. If it is not mergeable
1499 repository if the pull request is mergeable. If it is not mergeable
1283 the link should not be displayed.
1500 the link should not be displayed.
1284 """
1501 """
1285 pull_request = pr_util.create_pull_request(
1502 pull_request = pr_util.create_pull_request(
1286 mergeable=mergeable, enable_notifications=False)
1503 mergeable=mergeable, enable_notifications=False)
1287 target_repo = pull_request.target_repo.scm_instance()
1504 target_repo = pull_request.target_repo.scm_instance()
1288 pr_id = pull_request.pull_request_id
1505 pr_id = pull_request.pull_request_id
1289 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1506 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1290 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1507 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1291
1508
1292 response = self.app.get(route_path(
1509 response = self.app.get(route_path(
1293 'pullrequest_show',
1510 'pullrequest_show',
1294 repo_name=target_repo.name,
1511 repo_name=target_repo.name,
1295 pull_request_id=pr_id))
1512 pull_request_id=pr_id))
1296
1513
1297 if mergeable:
1514 if mergeable:
1298 response.assert_response().element_value_contains(
1515 response.assert_response().element_value_contains(
1299 'input.pr-mergeinfo', shadow_url)
1516 'input.pr-mergeinfo', shadow_url)
1300 response.assert_response().element_value_contains(
1517 response.assert_response().element_value_contains(
1301 'input.pr-mergeinfo ', 'pr-merge')
1518 'input.pr-mergeinfo ', 'pr-merge')
1302 else:
1519 else:
1303 response.assert_response().no_element_exists('.pr-mergeinfo')
1520 response.assert_response().no_element_exists('.pr-mergeinfo')
1304
1521
1305
1522
1306 @pytest.mark.usefixtures('app')
1523 @pytest.mark.usefixtures('app')
1307 @pytest.mark.backends("git", "hg")
1524 @pytest.mark.backends("git", "hg")
1308 class TestPullrequestsControllerDelete(object):
1525 class TestPullrequestsControllerDelete(object):
1309 def test_pull_request_delete_button_permissions_admin(
1526 def test_pull_request_delete_button_permissions_admin(
1310 self, autologin_user, user_admin, pr_util):
1527 self, autologin_user, user_admin, pr_util):
1311 pull_request = pr_util.create_pull_request(
1528 pull_request = pr_util.create_pull_request(
1312 author=user_admin.username, enable_notifications=False)
1529 author=user_admin.username, enable_notifications=False)
1313
1530
1314 response = self.app.get(route_path(
1531 response = self.app.get(route_path(
1315 'pullrequest_show',
1532 'pullrequest_show',
1316 repo_name=pull_request.target_repo.scm_instance().name,
1533 repo_name=pull_request.target_repo.scm_instance().name,
1317 pull_request_id=pull_request.pull_request_id))
1534 pull_request_id=pull_request.pull_request_id))
1318
1535
1319 response.mustcontain('id="delete_pullrequest"')
1536 response.mustcontain('id="delete_pullrequest"')
1320 response.mustcontain('Confirm to delete this pull request')
1537 response.mustcontain('Confirm to delete this pull request')
1321
1538
1322 def test_pull_request_delete_button_permissions_owner(
1539 def test_pull_request_delete_button_permissions_owner(
1323 self, autologin_regular_user, user_regular, pr_util):
1540 self, autologin_regular_user, user_regular, pr_util):
1324 pull_request = pr_util.create_pull_request(
1541 pull_request = pr_util.create_pull_request(
1325 author=user_regular.username, enable_notifications=False)
1542 author=user_regular.username, enable_notifications=False)
1326
1543
1327 response = self.app.get(route_path(
1544 response = self.app.get(route_path(
1328 'pullrequest_show',
1545 'pullrequest_show',
1329 repo_name=pull_request.target_repo.scm_instance().name,
1546 repo_name=pull_request.target_repo.scm_instance().name,
1330 pull_request_id=pull_request.pull_request_id))
1547 pull_request_id=pull_request.pull_request_id))
1331
1548
1332 response.mustcontain('id="delete_pullrequest"')
1549 response.mustcontain('id="delete_pullrequest"')
1333 response.mustcontain('Confirm to delete this pull request')
1550 response.mustcontain('Confirm to delete this pull request')
1334
1551
1335 def test_pull_request_delete_button_permissions_forbidden(
1552 def test_pull_request_delete_button_permissions_forbidden(
1336 self, autologin_regular_user, user_regular, user_admin, pr_util):
1553 self, autologin_regular_user, user_regular, user_admin, pr_util):
1337 pull_request = pr_util.create_pull_request(
1554 pull_request = pr_util.create_pull_request(
1338 author=user_admin.username, enable_notifications=False)
1555 author=user_admin.username, enable_notifications=False)
1339
1556
1340 response = self.app.get(route_path(
1557 response = self.app.get(route_path(
1341 'pullrequest_show',
1558 'pullrequest_show',
1342 repo_name=pull_request.target_repo.scm_instance().name,
1559 repo_name=pull_request.target_repo.scm_instance().name,
1343 pull_request_id=pull_request.pull_request_id))
1560 pull_request_id=pull_request.pull_request_id))
1344 response.mustcontain(no=['id="delete_pullrequest"'])
1561 response.mustcontain(no=['id="delete_pullrequest"'])
1345 response.mustcontain(no=['Confirm to delete this pull request'])
1562 response.mustcontain(no=['Confirm to delete this pull request'])
1346
1563
1347 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1564 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1348 self, autologin_regular_user, user_regular, user_admin, pr_util,
1565 self, autologin_regular_user, user_regular, user_admin, pr_util,
1349 user_util):
1566 user_util):
1350
1567
1351 pull_request = pr_util.create_pull_request(
1568 pull_request = pr_util.create_pull_request(
1352 author=user_admin.username, enable_notifications=False)
1569 author=user_admin.username, enable_notifications=False)
1353
1570
1354 user_util.grant_user_permission_to_repo(
1571 user_util.grant_user_permission_to_repo(
1355 pull_request.target_repo, user_regular,
1572 pull_request.target_repo, user_regular,
1356 'repository.write')
1573 'repository.write')
1357
1574
1358 response = self.app.get(route_path(
1575 response = self.app.get(route_path(
1359 'pullrequest_show',
1576 'pullrequest_show',
1360 repo_name=pull_request.target_repo.scm_instance().name,
1577 repo_name=pull_request.target_repo.scm_instance().name,
1361 pull_request_id=pull_request.pull_request_id))
1578 pull_request_id=pull_request.pull_request_id))
1362
1579
1363 response.mustcontain('id="open_edit_pullrequest"')
1580 response.mustcontain('id="open_edit_pullrequest"')
1364 response.mustcontain('id="delete_pullrequest"')
1581 response.mustcontain('id="delete_pullrequest"')
1365 response.mustcontain(no=['Confirm to delete this pull request'])
1582 response.mustcontain(no=['Confirm to delete this pull request'])
1366
1583
1367 def test_delete_comment_returns_404_if_comment_does_not_exist(
1584 def test_delete_comment_returns_404_if_comment_does_not_exist(
1368 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1585 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1369
1586
1370 pull_request = pr_util.create_pull_request(
1587 pull_request = pr_util.create_pull_request(
1371 author=user_admin.username, enable_notifications=False)
1588 author=user_admin.username, enable_notifications=False)
1372
1589
1373 self.app.post(
1590 self.app.post(
1374 route_path(
1591 route_path(
1375 'pullrequest_comment_delete',
1592 'pullrequest_comment_delete',
1376 repo_name=pull_request.target_repo.scm_instance().name,
1593 repo_name=pull_request.target_repo.scm_instance().name,
1377 pull_request_id=pull_request.pull_request_id,
1594 pull_request_id=pull_request.pull_request_id,
1378 comment_id=1024404),
1595 comment_id=1024404),
1379 extra_environ=xhr_header,
1596 extra_environ=xhr_header,
1380 params={'csrf_token': csrf_token},
1597 params={'csrf_token': csrf_token},
1381 status=404
1598 status=404
1382 )
1599 )
1383
1600
1384 def test_delete_comment(
1601 def test_delete_comment(
1385 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1602 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1386
1603
1387 pull_request = pr_util.create_pull_request(
1604 pull_request = pr_util.create_pull_request(
1388 author=user_admin.username, enable_notifications=False)
1605 author=user_admin.username, enable_notifications=False)
1389 comment = pr_util.create_comment()
1606 comment = pr_util.create_comment()
1390 comment_id = comment.comment_id
1607 comment_id = comment.comment_id
1391
1608
1392 response = self.app.post(
1609 response = self.app.post(
1393 route_path(
1610 route_path(
1394 'pullrequest_comment_delete',
1611 'pullrequest_comment_delete',
1395 repo_name=pull_request.target_repo.scm_instance().name,
1612 repo_name=pull_request.target_repo.scm_instance().name,
1396 pull_request_id=pull_request.pull_request_id,
1613 pull_request_id=pull_request.pull_request_id,
1397 comment_id=comment_id),
1614 comment_id=comment_id),
1398 extra_environ=xhr_header,
1615 extra_environ=xhr_header,
1399 params={'csrf_token': csrf_token},
1616 params={'csrf_token': csrf_token},
1400 status=200
1617 status=200
1401 )
1618 )
1402 assert response.body == 'true'
1619 assert response.body == 'true'
1403
1620
1404 @pytest.mark.parametrize('url_type', [
1621 @pytest.mark.parametrize('url_type', [
1405 'pullrequest_new',
1622 'pullrequest_new',
1406 'pullrequest_create',
1623 'pullrequest_create',
1407 'pullrequest_update',
1624 'pullrequest_update',
1408 'pullrequest_merge',
1625 'pullrequest_merge',
1409 ])
1626 ])
1410 def test_pull_request_is_forbidden_on_archived_repo(
1627 def test_pull_request_is_forbidden_on_archived_repo(
1411 self, autologin_user, backend, xhr_header, user_util, url_type):
1628 self, autologin_user, backend, xhr_header, user_util, url_type):
1412
1629
1413 # create a temporary repo
1630 # create a temporary repo
1414 source = user_util.create_repo(repo_type=backend.alias)
1631 source = user_util.create_repo(repo_type=backend.alias)
1415 repo_name = source.repo_name
1632 repo_name = source.repo_name
1416 repo = Repository.get_by_repo_name(repo_name)
1633 repo = Repository.get_by_repo_name(repo_name)
1417 repo.archived = True
1634 repo.archived = True
1418 Session().commit()
1635 Session().commit()
1419
1636
1420 response = self.app.get(
1637 response = self.app.get(
1421 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1638 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1422
1639
1423 msg = 'Action not supported for archived repository.'
1640 msg = 'Action not supported for archived repository.'
1424 assert_session_flash(response, msg)
1641 assert_session_flash(response, msg)
1425
1642
1426
1643
1427 def assert_pull_request_status(pull_request, expected_status):
1644 def assert_pull_request_status(pull_request, expected_status):
1428 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1645 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1429 assert status == expected_status
1646 assert status == expected_status
1430
1647
1431
1648
1432 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1649 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1433 @pytest.mark.usefixtures("autologin_user")
1650 @pytest.mark.usefixtures("autologin_user")
1434 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1651 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1435 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1652 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,1617 +1,1626 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
65 c.renderer = 'plain'
65 c.renderer = 'plain'
66 return c
66 return c
67
67
68 def _get_pull_requests_list(
68 def _get_pull_requests_list(
69 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
70
70
71 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
74 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
75
75
76 # pagination
76 # pagination
77
77
78 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
79 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 statuses=statuses, offset=start, length=limit,
81 statuses=statuses, offset=start, length=limit,
82 order_by=order_by, order_dir=order_dir)
82 order_by=order_by, order_dir=order_dir)
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 repo_name, search_q=search_q, source=source, statuses=statuses,
84 repo_name, search_q=search_q, source=source, statuses=statuses,
85 opened_by=opened_by)
85 opened_by=opened_by)
86 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
87 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 offset=start, length=limit, order_by=order_by,
90 offset=start, length=limit, order_by=order_by,
91 order_dir=order_dir)
91 order_dir=order_dir)
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 statuses=statuses, opened_by=opened_by)
94 statuses=statuses, opened_by=opened_by)
95 else:
95 else:
96 pull_requests = PullRequestModel().get_all(
96 pull_requests = PullRequestModel().get_all(
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 statuses=statuses, offset=start, length=limit,
98 statuses=statuses, offset=start, length=limit,
99 order_by=order_by, order_dir=order_dir)
99 order_by=order_by, order_dir=order_dir)
100 pull_requests_total_count = PullRequestModel().count_all(
100 pull_requests_total_count = PullRequestModel().count_all(
101 repo_name, search_q=search_q, source=source, statuses=statuses,
101 repo_name, search_q=search_q, source=source, statuses=statuses,
102 opened_by=opened_by)
102 opened_by=opened_by)
103
103
104 data = []
104 data = []
105 comments_model = CommentsModel()
105 comments_model = CommentsModel()
106 for pr in pull_requests:
106 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
107 comments = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
108 self.db_repo.repo_id, pull_request=pr)
109
109
110 data.append({
110 data.append({
111 'name': _render('pullrequest_name',
111 'name': _render('pullrequest_name',
112 pr.pull_request_id, pr.pull_request_state,
112 pr.pull_request_id, pr.pull_request_state,
113 pr.work_in_progress, pr.target_repo.repo_name),
113 pr.work_in_progress, pr.target_repo.repo_name),
114 'name_raw': pr.pull_request_id,
114 'name_raw': pr.pull_request_id,
115 'status': _render('pullrequest_status',
115 'status': _render('pullrequest_status',
116 pr.calculated_review_status()),
116 pr.calculated_review_status()),
117 'title': _render('pullrequest_title', pr.title, pr.description),
117 'title': _render('pullrequest_title', pr.title, pr.description),
118 'description': h.escape(pr.description),
118 'description': h.escape(pr.description),
119 'updated_on': _render('pullrequest_updated_on',
119 'updated_on': _render('pullrequest_updated_on',
120 h.datetime_to_time(pr.updated_on)),
120 h.datetime_to_time(pr.updated_on)),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'created_on': _render('pullrequest_updated_on',
122 'created_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.created_on)),
123 h.datetime_to_time(pr.created_on)),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'state': pr.pull_request_state,
125 'state': pr.pull_request_state,
126 'author': _render('pullrequest_author',
126 'author': _render('pullrequest_author',
127 pr.author.full_contact, ),
127 pr.author.full_contact, ),
128 'author_raw': pr.author.full_name,
128 'author_raw': pr.author.full_name,
129 'comments': _render('pullrequest_comments', len(comments)),
129 'comments': _render('pullrequest_comments', len(comments)),
130 'comments_raw': len(comments),
130 'comments_raw': len(comments),
131 'closed': pr.is_closed(),
131 'closed': pr.is_closed(),
132 })
132 })
133
133
134 data = ({
134 data = ({
135 'draw': draw,
135 'draw': draw,
136 'data': data,
136 'data': data,
137 'recordsTotal': pull_requests_total_count,
137 'recordsTotal': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
139 })
139 })
140 return data
140 return data
141
141
142 @LoginRequired()
142 @LoginRequired()
143 @HasRepoPermissionAnyDecorator(
143 @HasRepoPermissionAnyDecorator(
144 'repository.read', 'repository.write', 'repository.admin')
144 'repository.read', 'repository.write', 'repository.admin')
145 @view_config(
145 @view_config(
146 route_name='pullrequest_show_all', request_method='GET',
146 route_name='pullrequest_show_all', request_method='GET',
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 def pull_request_list(self):
148 def pull_request_list(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150
150
151 req_get = self.request.GET
151 req_get = self.request.GET
152 c.source = str2bool(req_get.get('source'))
152 c.source = str2bool(req_get.get('source'))
153 c.closed = str2bool(req_get.get('closed'))
153 c.closed = str2bool(req_get.get('closed'))
154 c.my = str2bool(req_get.get('my'))
154 c.my = str2bool(req_get.get('my'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157
157
158 c.active = 'open'
158 c.active = 'open'
159 if c.my:
159 if c.my:
160 c.active = 'my'
160 c.active = 'my'
161 if c.closed:
161 if c.closed:
162 c.active = 'closed'
162 c.active = 'closed'
163 if c.awaiting_review and not c.source:
163 if c.awaiting_review and not c.source:
164 c.active = 'awaiting'
164 c.active = 'awaiting'
165 if c.source and not c.awaiting_review:
165 if c.source and not c.awaiting_review:
166 c.active = 'source'
166 c.active = 'source'
167 if c.awaiting_my_review:
167 if c.awaiting_my_review:
168 c.active = 'awaiting_my'
168 c.active = 'awaiting_my'
169
169
170 return self._get_template_context(c)
170 return self._get_template_context(c)
171
171
172 @LoginRequired()
172 @LoginRequired()
173 @HasRepoPermissionAnyDecorator(
173 @HasRepoPermissionAnyDecorator(
174 'repository.read', 'repository.write', 'repository.admin')
174 'repository.read', 'repository.write', 'repository.admin')
175 @view_config(
175 @view_config(
176 route_name='pullrequest_show_all_data', request_method='GET',
176 route_name='pullrequest_show_all_data', request_method='GET',
177 renderer='json_ext', xhr=True)
177 renderer='json_ext', xhr=True)
178 def pull_request_list_data(self):
178 def pull_request_list_data(self):
179 self.load_default_context()
179 self.load_default_context()
180
180
181 # additional filters
181 # additional filters
182 req_get = self.request.GET
182 req_get = self.request.GET
183 source = str2bool(req_get.get('source'))
183 source = str2bool(req_get.get('source'))
184 closed = str2bool(req_get.get('closed'))
184 closed = str2bool(req_get.get('closed'))
185 my = str2bool(req_get.get('my'))
185 my = str2bool(req_get.get('my'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188
188
189 filter_type = 'awaiting_review' if awaiting_review \
189 filter_type = 'awaiting_review' if awaiting_review \
190 else 'awaiting_my_review' if awaiting_my_review \
190 else 'awaiting_my_review' if awaiting_my_review \
191 else None
191 else None
192
192
193 opened_by = None
193 opened_by = None
194 if my:
194 if my:
195 opened_by = [self._rhodecode_user.user_id]
195 opened_by = [self._rhodecode_user.user_id]
196
196
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 if closed:
198 if closed:
199 statuses = [PullRequest.STATUS_CLOSED]
199 statuses = [PullRequest.STATUS_CLOSED]
200
200
201 data = self._get_pull_requests_list(
201 data = self._get_pull_requests_list(
202 repo_name=self.db_repo_name, source=source,
202 repo_name=self.db_repo_name, source=source,
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204
204
205 return data
205 return data
206
206
207 def _is_diff_cache_enabled(self, target_repo):
207 def _is_diff_cache_enabled(self, target_repo):
208 caching_enabled = self._get_general_setting(
208 caching_enabled = self._get_general_setting(
209 target_repo, 'rhodecode_diff_cache')
209 target_repo, 'rhodecode_diff_cache')
210 log.debug('Diff caching enabled: %s', caching_enabled)
210 log.debug('Diff caching enabled: %s', caching_enabled)
211 return caching_enabled
211 return caching_enabled
212
212
213 def _get_diffset(self, source_repo_name, source_repo,
213 def _get_diffset(self, source_repo_name, source_repo,
214 ancestor_commit,
214 ancestor_commit,
215 source_ref_id, target_ref_id,
215 source_ref_id, target_ref_id,
216 target_commit, source_commit, diff_limit, file_limit,
216 target_commit, source_commit, diff_limit, file_limit,
217 fulldiff, hide_whitespace_changes, diff_context):
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218
218
219 target_ref_id = ancestor_commit.raw_id
219 if use_ancestor:
220 # we might want to not use it for versions
221 target_ref_id = ancestor_commit.raw_id
222
220 vcs_diff = PullRequestModel().get_diff(
223 vcs_diff = PullRequestModel().get_diff(
221 source_repo, source_ref_id, target_ref_id,
224 source_repo, source_ref_id, target_ref_id,
222 hide_whitespace_changes, diff_context)
225 hide_whitespace_changes, diff_context)
223
226
224 diff_processor = diffs.DiffProcessor(
227 diff_processor = diffs.DiffProcessor(
225 vcs_diff, format='newdiff', diff_limit=diff_limit,
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
226 file_limit=file_limit, show_full_diff=fulldiff)
229 file_limit=file_limit, show_full_diff=fulldiff)
227
230
228 _parsed = diff_processor.prepare()
231 _parsed = diff_processor.prepare()
229
232
230 diffset = codeblocks.DiffSet(
233 diffset = codeblocks.DiffSet(
231 repo_name=self.db_repo_name,
234 repo_name=self.db_repo_name,
232 source_repo_name=source_repo_name,
235 source_repo_name=source_repo_name,
233 source_node_getter=codeblocks.diffset_node_getter(target_commit),
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
234 target_node_getter=codeblocks.diffset_node_getter(source_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
235 )
238 )
236 diffset = self.path_filter.render_patchset_filtered(
239 diffset = self.path_filter.render_patchset_filtered(
237 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
238
241
239 return diffset
242 return diffset
240
243
241 def _get_range_diffset(self, source_scm, source_repo,
244 def _get_range_diffset(self, source_scm, source_repo,
242 commit1, commit2, diff_limit, file_limit,
245 commit1, commit2, diff_limit, file_limit,
243 fulldiff, hide_whitespace_changes, diff_context):
246 fulldiff, hide_whitespace_changes, diff_context):
244 vcs_diff = source_scm.get_diff(
247 vcs_diff = source_scm.get_diff(
245 commit1, commit2,
248 commit1, commit2,
246 ignore_whitespace=hide_whitespace_changes,
249 ignore_whitespace=hide_whitespace_changes,
247 context=diff_context)
250 context=diff_context)
248
251
249 diff_processor = diffs.DiffProcessor(
252 diff_processor = diffs.DiffProcessor(
250 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
251 file_limit=file_limit, show_full_diff=fulldiff)
254 file_limit=file_limit, show_full_diff=fulldiff)
252
255
253 _parsed = diff_processor.prepare()
256 _parsed = diff_processor.prepare()
254
257
255 diffset = codeblocks.DiffSet(
258 diffset = codeblocks.DiffSet(
256 repo_name=source_repo.repo_name,
259 repo_name=source_repo.repo_name,
257 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
258 target_node_getter=codeblocks.diffset_node_getter(commit2))
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
259
262
260 diffset = self.path_filter.render_patchset_filtered(
263 diffset = self.path_filter.render_patchset_filtered(
261 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
262
265
263 return diffset
266 return diffset
264
267
265 @LoginRequired()
268 @LoginRequired()
266 @HasRepoPermissionAnyDecorator(
269 @HasRepoPermissionAnyDecorator(
267 'repository.read', 'repository.write', 'repository.admin')
270 'repository.read', 'repository.write', 'repository.admin')
268 @view_config(
271 @view_config(
269 route_name='pullrequest_show', request_method='GET',
272 route_name='pullrequest_show', request_method='GET',
270 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
271 def pull_request_show(self):
274 def pull_request_show(self):
272 _ = self.request.translate
275 _ = self.request.translate
273 c = self.load_default_context()
276 c = self.load_default_context()
274
277
275 pull_request = PullRequest.get_or_404(
278 pull_request = PullRequest.get_or_404(
276 self.request.matchdict['pull_request_id'])
279 self.request.matchdict['pull_request_id'])
277 pull_request_id = pull_request.pull_request_id
280 pull_request_id = pull_request.pull_request_id
278
281
279 c.state_progressing = pull_request.is_state_changing()
282 c.state_progressing = pull_request.is_state_changing()
280
283
281 _new_state = {
284 _new_state = {
282 'created': PullRequest.STATE_CREATED,
285 'created': PullRequest.STATE_CREATED,
283 }.get(self.request.GET.get('force_state'))
286 }.get(self.request.GET.get('force_state'))
284
287
285 if c.is_super_admin and _new_state:
288 if c.is_super_admin and _new_state:
286 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
289 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
287 h.flash(
290 h.flash(
288 _('Pull Request state was force changed to `{}`').format(_new_state),
291 _('Pull Request state was force changed to `{}`').format(_new_state),
289 category='success')
292 category='success')
290 Session().commit()
293 Session().commit()
291
294
292 raise HTTPFound(h.route_path(
295 raise HTTPFound(h.route_path(
293 'pullrequest_show', repo_name=self.db_repo_name,
296 'pullrequest_show', repo_name=self.db_repo_name,
294 pull_request_id=pull_request_id))
297 pull_request_id=pull_request_id))
295
298
296 version = self.request.GET.get('version')
299 version = self.request.GET.get('version')
297 from_version = self.request.GET.get('from_version') or version
300 from_version = self.request.GET.get('from_version') or version
298 merge_checks = self.request.GET.get('merge_checks')
301 merge_checks = self.request.GET.get('merge_checks')
299 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
302 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
300
303
301 # fetch global flags of ignore ws or context lines
304 # fetch global flags of ignore ws or context lines
302 diff_context = diffs.get_diff_context(self.request)
305 diff_context = diffs.get_diff_context(self.request)
303 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
306 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
304
307
305 force_refresh = str2bool(self.request.GET.get('force_refresh'))
308 force_refresh = str2bool(self.request.GET.get('force_refresh'))
306
309
307 (pull_request_latest,
310 (pull_request_latest,
308 pull_request_at_ver,
311 pull_request_at_ver,
309 pull_request_display_obj,
312 pull_request_display_obj,
310 at_version) = PullRequestModel().get_pr_version(
313 at_version) = PullRequestModel().get_pr_version(
311 pull_request_id, version=version)
314 pull_request_id, version=version)
312 pr_closed = pull_request_latest.is_closed()
315 pr_closed = pull_request_latest.is_closed()
313
316
314 if pr_closed and (version or from_version):
317 if pr_closed and (version or from_version):
315 # not allow to browse versions
318 # not allow to browse versions
316 raise HTTPFound(h.route_path(
319 raise HTTPFound(h.route_path(
317 'pullrequest_show', repo_name=self.db_repo_name,
320 'pullrequest_show', repo_name=self.db_repo_name,
318 pull_request_id=pull_request_id))
321 pull_request_id=pull_request_id))
319
322
320 versions = pull_request_display_obj.versions()
323 versions = pull_request_display_obj.versions()
321 # used to store per-commit range diffs
324 # used to store per-commit range diffs
322 c.changes = collections.OrderedDict()
325 c.changes = collections.OrderedDict()
323 c.range_diff_on = self.request.GET.get('range-diff') == "1"
326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
324
327
325 c.at_version = at_version
328 c.at_version = at_version
326 c.at_version_num = (at_version
329 c.at_version_num = (at_version
327 if at_version and at_version != 'latest'
330 if at_version and at_version != 'latest'
328 else None)
331 else None)
329 c.at_version_pos = ChangesetComment.get_index_from_version(
332 c.at_version_pos = ChangesetComment.get_index_from_version(
330 c.at_version_num, versions)
333 c.at_version_num, versions)
331
334
332 (prev_pull_request_latest,
335 (prev_pull_request_latest,
333 prev_pull_request_at_ver,
336 prev_pull_request_at_ver,
334 prev_pull_request_display_obj,
337 prev_pull_request_display_obj,
335 prev_at_version) = PullRequestModel().get_pr_version(
338 prev_at_version) = PullRequestModel().get_pr_version(
336 pull_request_id, version=from_version)
339 pull_request_id, version=from_version)
337
340
338 c.from_version = prev_at_version
341 c.from_version = prev_at_version
339 c.from_version_num = (prev_at_version
342 c.from_version_num = (prev_at_version
340 if prev_at_version and prev_at_version != 'latest'
343 if prev_at_version and prev_at_version != 'latest'
341 else None)
344 else None)
342 c.from_version_pos = ChangesetComment.get_index_from_version(
345 c.from_version_pos = ChangesetComment.get_index_from_version(
343 c.from_version_num, versions)
346 c.from_version_num, versions)
344
347
345 # define if we're in COMPARE mode or VIEW at version mode
348 # define if we're in COMPARE mode or VIEW at version mode
346 compare = at_version != prev_at_version
349 compare = at_version != prev_at_version
347
350
348 # pull_requests repo_name we opened it against
351 # pull_requests repo_name we opened it against
349 # ie. target_repo must match
352 # ie. target_repo must match
350 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
353 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
351 raise HTTPNotFound()
354 raise HTTPNotFound()
352
355
353 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
356 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
354 pull_request_at_ver)
357 pull_request_at_ver)
355
358
356 c.pull_request = pull_request_display_obj
359 c.pull_request = pull_request_display_obj
357 c.renderer = pull_request_at_ver.description_renderer or c.renderer
360 c.renderer = pull_request_at_ver.description_renderer or c.renderer
358 c.pull_request_latest = pull_request_latest
361 c.pull_request_latest = pull_request_latest
359
362
360 if compare or (at_version and not at_version == 'latest'):
363 if compare or (at_version and not at_version == 'latest'):
361 c.allowed_to_change_status = False
364 c.allowed_to_change_status = False
362 c.allowed_to_update = False
365 c.allowed_to_update = False
363 c.allowed_to_merge = False
366 c.allowed_to_merge = False
364 c.allowed_to_delete = False
367 c.allowed_to_delete = False
365 c.allowed_to_comment = False
368 c.allowed_to_comment = False
366 c.allowed_to_close = False
369 c.allowed_to_close = False
367 else:
370 else:
368 can_change_status = PullRequestModel().check_user_change_status(
371 can_change_status = PullRequestModel().check_user_change_status(
369 pull_request_at_ver, self._rhodecode_user)
372 pull_request_at_ver, self._rhodecode_user)
370 c.allowed_to_change_status = can_change_status and not pr_closed
373 c.allowed_to_change_status = can_change_status and not pr_closed
371
374
372 c.allowed_to_update = PullRequestModel().check_user_update(
375 c.allowed_to_update = PullRequestModel().check_user_update(
373 pull_request_latest, self._rhodecode_user) and not pr_closed
376 pull_request_latest, self._rhodecode_user) and not pr_closed
374 c.allowed_to_merge = PullRequestModel().check_user_merge(
377 c.allowed_to_merge = PullRequestModel().check_user_merge(
375 pull_request_latest, self._rhodecode_user) and not pr_closed
378 pull_request_latest, self._rhodecode_user) and not pr_closed
376 c.allowed_to_delete = PullRequestModel().check_user_delete(
379 c.allowed_to_delete = PullRequestModel().check_user_delete(
377 pull_request_latest, self._rhodecode_user) and not pr_closed
380 pull_request_latest, self._rhodecode_user) and not pr_closed
378 c.allowed_to_comment = not pr_closed
381 c.allowed_to_comment = not pr_closed
379 c.allowed_to_close = c.allowed_to_merge and not pr_closed
382 c.allowed_to_close = c.allowed_to_merge and not pr_closed
380
383
381 c.forbid_adding_reviewers = False
384 c.forbid_adding_reviewers = False
382 c.forbid_author_to_review = False
385 c.forbid_author_to_review = False
383 c.forbid_commit_author_to_review = False
386 c.forbid_commit_author_to_review = False
384
387
385 if pull_request_latest.reviewer_data and \
388 if pull_request_latest.reviewer_data and \
386 'rules' in pull_request_latest.reviewer_data:
389 'rules' in pull_request_latest.reviewer_data:
387 rules = pull_request_latest.reviewer_data['rules'] or {}
390 rules = pull_request_latest.reviewer_data['rules'] or {}
388 try:
391 try:
389 c.forbid_adding_reviewers = rules.get(
392 c.forbid_adding_reviewers = rules.get(
390 'forbid_adding_reviewers')
393 'forbid_adding_reviewers')
391 c.forbid_author_to_review = rules.get(
394 c.forbid_author_to_review = rules.get(
392 'forbid_author_to_review')
395 'forbid_author_to_review')
393 c.forbid_commit_author_to_review = rules.get(
396 c.forbid_commit_author_to_review = rules.get(
394 'forbid_commit_author_to_review')
397 'forbid_commit_author_to_review')
395 except Exception:
398 except Exception:
396 pass
399 pass
397
400
398 # check merge capabilities
401 # check merge capabilities
399 _merge_check = MergeCheck.validate(
402 _merge_check = MergeCheck.validate(
400 pull_request_latest, auth_user=self._rhodecode_user,
403 pull_request_latest, auth_user=self._rhodecode_user,
401 translator=self.request.translate,
404 translator=self.request.translate,
402 force_shadow_repo_refresh=force_refresh)
405 force_shadow_repo_refresh=force_refresh)
403
406
404 c.pr_merge_errors = _merge_check.error_details
407 c.pr_merge_errors = _merge_check.error_details
405 c.pr_merge_possible = not _merge_check.failed
408 c.pr_merge_possible = not _merge_check.failed
406 c.pr_merge_message = _merge_check.merge_msg
409 c.pr_merge_message = _merge_check.merge_msg
407 c.pr_merge_source_commit = _merge_check.source_commit
410 c.pr_merge_source_commit = _merge_check.source_commit
408 c.pr_merge_target_commit = _merge_check.target_commit
411 c.pr_merge_target_commit = _merge_check.target_commit
409
412
410 c.pr_merge_info = MergeCheck.get_merge_conditions(
413 c.pr_merge_info = MergeCheck.get_merge_conditions(
411 pull_request_latest, translator=self.request.translate)
414 pull_request_latest, translator=self.request.translate)
412
415
413 c.pull_request_review_status = _merge_check.review_status
416 c.pull_request_review_status = _merge_check.review_status
414 if merge_checks:
417 if merge_checks:
415 self.request.override_renderer = \
418 self.request.override_renderer = \
416 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
419 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
417 return self._get_template_context(c)
420 return self._get_template_context(c)
418
421
419 comments_model = CommentsModel()
422 comments_model = CommentsModel()
420
423
421 # reviewers and statuses
424 # reviewers and statuses
422 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
425 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
423 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
426 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
424
427
425 # GENERAL COMMENTS with versions #
428 # GENERAL COMMENTS with versions #
426 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
429 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
427 q = q.order_by(ChangesetComment.comment_id.asc())
430 q = q.order_by(ChangesetComment.comment_id.asc())
428 general_comments = q
431 general_comments = q
429
432
430 # pick comments we want to render at current version
433 # pick comments we want to render at current version
431 c.comment_versions = comments_model.aggregate_comments(
434 c.comment_versions = comments_model.aggregate_comments(
432 general_comments, versions, c.at_version_num)
435 general_comments, versions, c.at_version_num)
433 c.comments = c.comment_versions[c.at_version_num]['until']
436 c.comments = c.comment_versions[c.at_version_num]['until']
434
437
435 # INLINE COMMENTS with versions #
438 # INLINE COMMENTS with versions #
436 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
439 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
437 q = q.order_by(ChangesetComment.comment_id.asc())
440 q = q.order_by(ChangesetComment.comment_id.asc())
438 inline_comments = q
441 inline_comments = q
439
442
440 c.inline_versions = comments_model.aggregate_comments(
443 c.inline_versions = comments_model.aggregate_comments(
441 inline_comments, versions, c.at_version_num, inline=True)
444 inline_comments, versions, c.at_version_num, inline=True)
442
445
443 # TODOs
446 # TODOs
444 c.unresolved_comments = CommentsModel() \
447 c.unresolved_comments = CommentsModel() \
445 .get_pull_request_unresolved_todos(pull_request)
448 .get_pull_request_unresolved_todos(pull_request)
446 c.resolved_comments = CommentsModel() \
449 c.resolved_comments = CommentsModel() \
447 .get_pull_request_resolved_todos(pull_request)
450 .get_pull_request_resolved_todos(pull_request)
448
451
449 # inject latest version
452 # inject latest version
450 latest_ver = PullRequest.get_pr_display_object(
453 latest_ver = PullRequest.get_pr_display_object(
451 pull_request_latest, pull_request_latest)
454 pull_request_latest, pull_request_latest)
452
455
453 c.versions = versions + [latest_ver]
456 c.versions = versions + [latest_ver]
454
457
455 # if we use version, then do not show later comments
458 # if we use version, then do not show later comments
456 # than current version
459 # than current version
457 display_inline_comments = collections.defaultdict(
460 display_inline_comments = collections.defaultdict(
458 lambda: collections.defaultdict(list))
461 lambda: collections.defaultdict(list))
459 for co in inline_comments:
462 for co in inline_comments:
460 if c.at_version_num:
463 if c.at_version_num:
461 # pick comments that are at least UPTO given version, so we
464 # pick comments that are at least UPTO given version, so we
462 # don't render comments for higher version
465 # don't render comments for higher version
463 should_render = co.pull_request_version_id and \
466 should_render = co.pull_request_version_id and \
464 co.pull_request_version_id <= c.at_version_num
467 co.pull_request_version_id <= c.at_version_num
465 else:
468 else:
466 # showing all, for 'latest'
469 # showing all, for 'latest'
467 should_render = True
470 should_render = True
468
471
469 if should_render:
472 if should_render:
470 display_inline_comments[co.f_path][co.line_no].append(co)
473 display_inline_comments[co.f_path][co.line_no].append(co)
471
474
472 # load diff data into template context, if we use compare mode then
475 # load diff data into template context, if we use compare mode then
473 # diff is calculated based on changes between versions of PR
476 # diff is calculated based on changes between versions of PR
474
477
475 source_repo = pull_request_at_ver.source_repo
478 source_repo = pull_request_at_ver.source_repo
476 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
479 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
477
480
478 target_repo = pull_request_at_ver.target_repo
481 target_repo = pull_request_at_ver.target_repo
479 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
482 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
480
483
481 if compare:
484 if compare:
482 # in compare switch the diff base to latest commit from prev version
485 # in compare switch the diff base to latest commit from prev version
483 target_ref_id = prev_pull_request_display_obj.revisions[0]
486 target_ref_id = prev_pull_request_display_obj.revisions[0]
484
487
485 # despite opening commits for bookmarks/branches/tags, we always
488 # despite opening commits for bookmarks/branches/tags, we always
486 # convert this to rev to prevent changes after bookmark or branch change
489 # convert this to rev to prevent changes after bookmark or branch change
487 c.source_ref_type = 'rev'
490 c.source_ref_type = 'rev'
488 c.source_ref = source_ref_id
491 c.source_ref = source_ref_id
489
492
490 c.target_ref_type = 'rev'
493 c.target_ref_type = 'rev'
491 c.target_ref = target_ref_id
494 c.target_ref = target_ref_id
492
495
493 c.source_repo = source_repo
496 c.source_repo = source_repo
494 c.target_repo = target_repo
497 c.target_repo = target_repo
495
498
496 c.commit_ranges = []
499 c.commit_ranges = []
497 source_commit = EmptyCommit()
500 source_commit = EmptyCommit()
498 target_commit = EmptyCommit()
501 target_commit = EmptyCommit()
499 c.missing_requirements = False
502 c.missing_requirements = False
500
503
501 source_scm = source_repo.scm_instance()
504 source_scm = source_repo.scm_instance()
502 target_scm = target_repo.scm_instance()
505 target_scm = target_repo.scm_instance()
503
506
504 shadow_scm = None
507 shadow_scm = None
505 try:
508 try:
506 shadow_scm = pull_request_latest.get_shadow_repo()
509 shadow_scm = pull_request_latest.get_shadow_repo()
507 except Exception:
510 except Exception:
508 log.debug('Failed to get shadow repo', exc_info=True)
511 log.debug('Failed to get shadow repo', exc_info=True)
509 # try first the existing source_repo, and then shadow
512 # try first the existing source_repo, and then shadow
510 # repo if we can obtain one
513 # repo if we can obtain one
511 commits_source_repo = source_scm
514 commits_source_repo = source_scm
512 if shadow_scm:
515 if shadow_scm:
513 commits_source_repo = shadow_scm
516 commits_source_repo = shadow_scm
514
517
515 c.commits_source_repo = commits_source_repo
518 c.commits_source_repo = commits_source_repo
516 c.ancestor = None # set it to None, to hide it from PR view
519 c.ancestor = None # set it to None, to hide it from PR view
517
520
518 # empty version means latest, so we keep this to prevent
521 # empty version means latest, so we keep this to prevent
519 # double caching
522 # double caching
520 version_normalized = version or 'latest'
523 version_normalized = version or 'latest'
521 from_version_normalized = from_version or 'latest'
524 from_version_normalized = from_version or 'latest'
522
525
523 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
526 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
524 cache_file_path = diff_cache_exist(
527 cache_file_path = diff_cache_exist(
525 cache_path, 'pull_request', pull_request_id, version_normalized,
528 cache_path, 'pull_request', pull_request_id, version_normalized,
526 from_version_normalized, source_ref_id, target_ref_id,
529 from_version_normalized, source_ref_id, target_ref_id,
527 hide_whitespace_changes, diff_context, c.fulldiff)
530 hide_whitespace_changes, diff_context, c.fulldiff)
528
531
529 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
532 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
530 force_recache = self.get_recache_flag()
533 force_recache = self.get_recache_flag()
531
534
532 cached_diff = None
535 cached_diff = None
533 if caching_enabled:
536 if caching_enabled:
534 cached_diff = load_cached_diff(cache_file_path)
537 cached_diff = load_cached_diff(cache_file_path)
535
538
536 has_proper_commit_cache = (
539 has_proper_commit_cache = (
537 cached_diff and cached_diff.get('commits')
540 cached_diff and cached_diff.get('commits')
538 and len(cached_diff.get('commits', [])) == 5
541 and len(cached_diff.get('commits', [])) == 5
539 and cached_diff.get('commits')[0]
542 and cached_diff.get('commits')[0]
540 and cached_diff.get('commits')[3])
543 and cached_diff.get('commits')[3])
541
544
542 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
545 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
543 diff_commit_cache = \
546 diff_commit_cache = \
544 (ancestor_commit, commit_cache, missing_requirements,
547 (ancestor_commit, commit_cache, missing_requirements,
545 source_commit, target_commit) = cached_diff['commits']
548 source_commit, target_commit) = cached_diff['commits']
546 else:
549 else:
547 # NOTE(marcink): we reach potentially unreachable errors when a PR has
550 # NOTE(marcink): we reach potentially unreachable errors when a PR has
548 # merge errors resulting in potentially hidden commits in the shadow repo.
551 # merge errors resulting in potentially hidden commits in the shadow repo.
549 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
552 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
550 and _merge_check.merge_response
553 and _merge_check.merge_response
551 maybe_unreachable = maybe_unreachable \
554 maybe_unreachable = maybe_unreachable \
552 and _merge_check.merge_response.metadata.get('unresolved_files')
555 and _merge_check.merge_response.metadata.get('unresolved_files')
553 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
556 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
554 diff_commit_cache = \
557 diff_commit_cache = \
555 (ancestor_commit, commit_cache, missing_requirements,
558 (ancestor_commit, commit_cache, missing_requirements,
556 source_commit, target_commit) = self.get_commits(
559 source_commit, target_commit) = self.get_commits(
557 commits_source_repo,
560 commits_source_repo,
558 pull_request_at_ver,
561 pull_request_at_ver,
559 source_commit,
562 source_commit,
560 source_ref_id,
563 source_ref_id,
561 source_scm,
564 source_scm,
562 target_commit,
565 target_commit,
563 target_ref_id,
566 target_ref_id,
564 target_scm,
567 target_scm,
565 maybe_unreachable=maybe_unreachable)
568 maybe_unreachable=maybe_unreachable)
566
569
567 # register our commit range
570 # register our commit range
568 for comm in commit_cache.values():
571 for comm in commit_cache.values():
569 c.commit_ranges.append(comm)
572 c.commit_ranges.append(comm)
570
573
571 c.missing_requirements = missing_requirements
574 c.missing_requirements = missing_requirements
572 c.ancestor_commit = ancestor_commit
575 c.ancestor_commit = ancestor_commit
573 c.statuses = source_repo.statuses(
576 c.statuses = source_repo.statuses(
574 [x.raw_id for x in c.commit_ranges])
577 [x.raw_id for x in c.commit_ranges])
575
578
576 # auto collapse if we have more than limit
579 # auto collapse if we have more than limit
577 collapse_limit = diffs.DiffProcessor._collapse_commits_over
580 collapse_limit = diffs.DiffProcessor._collapse_commits_over
578 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
581 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
579 c.compare_mode = compare
582 c.compare_mode = compare
580
583
581 # diff_limit is the old behavior, will cut off the whole diff
584 # diff_limit is the old behavior, will cut off the whole diff
582 # if the limit is applied otherwise will just hide the
585 # if the limit is applied otherwise will just hide the
583 # big files from the front-end
586 # big files from the front-end
584 diff_limit = c.visual.cut_off_limit_diff
587 diff_limit = c.visual.cut_off_limit_diff
585 file_limit = c.visual.cut_off_limit_file
588 file_limit = c.visual.cut_off_limit_file
586
589
587 c.missing_commits = False
590 c.missing_commits = False
588 if (c.missing_requirements
591 if (c.missing_requirements
589 or isinstance(source_commit, EmptyCommit)
592 or isinstance(source_commit, EmptyCommit)
590 or source_commit == target_commit):
593 or source_commit == target_commit):
591
594
592 c.missing_commits = True
595 c.missing_commits = True
593 else:
596 else:
594 c.inline_comments = display_inline_comments
597 c.inline_comments = display_inline_comments
595
598
599 use_ancestor = True
600 if from_version_normalized != version_normalized:
601 use_ancestor = False
602
596 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
603 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
597 if not force_recache and has_proper_diff_cache:
604 if not force_recache and has_proper_diff_cache:
598 c.diffset = cached_diff['diff']
605 c.diffset = cached_diff['diff']
599 else:
606 else:
600 c.diffset = self._get_diffset(
607 c.diffset = self._get_diffset(
601 c.source_repo.repo_name, commits_source_repo,
608 c.source_repo.repo_name, commits_source_repo,
602 c.ancestor_commit,
609 c.ancestor_commit,
603 source_ref_id, target_ref_id,
610 source_ref_id, target_ref_id,
604 target_commit, source_commit,
611 target_commit, source_commit,
605 diff_limit, file_limit, c.fulldiff,
612 diff_limit, file_limit, c.fulldiff,
606 hide_whitespace_changes, diff_context)
613 hide_whitespace_changes, diff_context,
614 use_ancestor=use_ancestor
615 )
607
616
608 # save cached diff
617 # save cached diff
609 if caching_enabled:
618 if caching_enabled:
610 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
619 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
611
620
612 c.limited_diff = c.diffset.limited_diff
621 c.limited_diff = c.diffset.limited_diff
613
622
614 # calculate removed files that are bound to comments
623 # calculate removed files that are bound to comments
615 comment_deleted_files = [
624 comment_deleted_files = [
616 fname for fname in display_inline_comments
625 fname for fname in display_inline_comments
617 if fname not in c.diffset.file_stats]
626 if fname not in c.diffset.file_stats]
618
627
619 c.deleted_files_comments = collections.defaultdict(dict)
628 c.deleted_files_comments = collections.defaultdict(dict)
620 for fname, per_line_comments in display_inline_comments.items():
629 for fname, per_line_comments in display_inline_comments.items():
621 if fname in comment_deleted_files:
630 if fname in comment_deleted_files:
622 c.deleted_files_comments[fname]['stats'] = 0
631 c.deleted_files_comments[fname]['stats'] = 0
623 c.deleted_files_comments[fname]['comments'] = list()
632 c.deleted_files_comments[fname]['comments'] = list()
624 for lno, comments in per_line_comments.items():
633 for lno, comments in per_line_comments.items():
625 c.deleted_files_comments[fname]['comments'].extend(comments)
634 c.deleted_files_comments[fname]['comments'].extend(comments)
626
635
627 # maybe calculate the range diff
636 # maybe calculate the range diff
628 if c.range_diff_on:
637 if c.range_diff_on:
629 # TODO(marcink): set whitespace/context
638 # TODO(marcink): set whitespace/context
630 context_lcl = 3
639 context_lcl = 3
631 ign_whitespace_lcl = False
640 ign_whitespace_lcl = False
632
641
633 for commit in c.commit_ranges:
642 for commit in c.commit_ranges:
634 commit2 = commit
643 commit2 = commit
635 commit1 = commit.first_parent
644 commit1 = commit.first_parent
636
645
637 range_diff_cache_file_path = diff_cache_exist(
646 range_diff_cache_file_path = diff_cache_exist(
638 cache_path, 'diff', commit.raw_id,
647 cache_path, 'diff', commit.raw_id,
639 ign_whitespace_lcl, context_lcl, c.fulldiff)
648 ign_whitespace_lcl, context_lcl, c.fulldiff)
640
649
641 cached_diff = None
650 cached_diff = None
642 if caching_enabled:
651 if caching_enabled:
643 cached_diff = load_cached_diff(range_diff_cache_file_path)
652 cached_diff = load_cached_diff(range_diff_cache_file_path)
644
653
645 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
654 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
646 if not force_recache and has_proper_diff_cache:
655 if not force_recache and has_proper_diff_cache:
647 diffset = cached_diff['diff']
656 diffset = cached_diff['diff']
648 else:
657 else:
649 diffset = self._get_range_diffset(
658 diffset = self._get_range_diffset(
650 commits_source_repo, source_repo,
659 commits_source_repo, source_repo,
651 commit1, commit2, diff_limit, file_limit,
660 commit1, commit2, diff_limit, file_limit,
652 c.fulldiff, ign_whitespace_lcl, context_lcl
661 c.fulldiff, ign_whitespace_lcl, context_lcl
653 )
662 )
654
663
655 # save cached diff
664 # save cached diff
656 if caching_enabled:
665 if caching_enabled:
657 cache_diff(range_diff_cache_file_path, diffset, None)
666 cache_diff(range_diff_cache_file_path, diffset, None)
658
667
659 c.changes[commit.raw_id] = diffset
668 c.changes[commit.raw_id] = diffset
660
669
661 # this is a hack to properly display links, when creating PR, the
670 # this is a hack to properly display links, when creating PR, the
662 # compare view and others uses different notation, and
671 # compare view and others uses different notation, and
663 # compare_commits.mako renders links based on the target_repo.
672 # compare_commits.mako renders links based on the target_repo.
664 # We need to swap that here to generate it properly on the html side
673 # We need to swap that here to generate it properly on the html side
665 c.target_repo = c.source_repo
674 c.target_repo = c.source_repo
666
675
667 c.commit_statuses = ChangesetStatus.STATUSES
676 c.commit_statuses = ChangesetStatus.STATUSES
668
677
669 c.show_version_changes = not pr_closed
678 c.show_version_changes = not pr_closed
670 if c.show_version_changes:
679 if c.show_version_changes:
671 cur_obj = pull_request_at_ver
680 cur_obj = pull_request_at_ver
672 prev_obj = prev_pull_request_at_ver
681 prev_obj = prev_pull_request_at_ver
673
682
674 old_commit_ids = prev_obj.revisions
683 old_commit_ids = prev_obj.revisions
675 new_commit_ids = cur_obj.revisions
684 new_commit_ids = cur_obj.revisions
676 commit_changes = PullRequestModel()._calculate_commit_id_changes(
685 commit_changes = PullRequestModel()._calculate_commit_id_changes(
677 old_commit_ids, new_commit_ids)
686 old_commit_ids, new_commit_ids)
678 c.commit_changes_summary = commit_changes
687 c.commit_changes_summary = commit_changes
679
688
680 # calculate the diff for commits between versions
689 # calculate the diff for commits between versions
681 c.commit_changes = []
690 c.commit_changes = []
682
691
683 def mark(cs, fw):
692 def mark(cs, fw):
684 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
693 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
685
694
686 for c_type, raw_id in mark(commit_changes.added, 'a') \
695 for c_type, raw_id in mark(commit_changes.added, 'a') \
687 + mark(commit_changes.removed, 'r') \
696 + mark(commit_changes.removed, 'r') \
688 + mark(commit_changes.common, 'c'):
697 + mark(commit_changes.common, 'c'):
689
698
690 if raw_id in commit_cache:
699 if raw_id in commit_cache:
691 commit = commit_cache[raw_id]
700 commit = commit_cache[raw_id]
692 else:
701 else:
693 try:
702 try:
694 commit = commits_source_repo.get_commit(raw_id)
703 commit = commits_source_repo.get_commit(raw_id)
695 except CommitDoesNotExistError:
704 except CommitDoesNotExistError:
696 # in case we fail extracting still use "dummy" commit
705 # in case we fail extracting still use "dummy" commit
697 # for display in commit diff
706 # for display in commit diff
698 commit = h.AttributeDict(
707 commit = h.AttributeDict(
699 {'raw_id': raw_id,
708 {'raw_id': raw_id,
700 'message': 'EMPTY or MISSING COMMIT'})
709 'message': 'EMPTY or MISSING COMMIT'})
701 c.commit_changes.append([c_type, commit])
710 c.commit_changes.append([c_type, commit])
702
711
703 # current user review statuses for each version
712 # current user review statuses for each version
704 c.review_versions = {}
713 c.review_versions = {}
705 if self._rhodecode_user.user_id in allowed_reviewers:
714 if self._rhodecode_user.user_id in allowed_reviewers:
706 for co in general_comments:
715 for co in general_comments:
707 if co.author.user_id == self._rhodecode_user.user_id:
716 if co.author.user_id == self._rhodecode_user.user_id:
708 status = co.status_change
717 status = co.status_change
709 if status:
718 if status:
710 _ver_pr = status[0].comment.pull_request_version_id
719 _ver_pr = status[0].comment.pull_request_version_id
711 c.review_versions[_ver_pr] = status[0]
720 c.review_versions[_ver_pr] = status[0]
712
721
713 return self._get_template_context(c)
722 return self._get_template_context(c)
714
723
715 def get_commits(
724 def get_commits(
716 self, commits_source_repo, pull_request_at_ver, source_commit,
725 self, commits_source_repo, pull_request_at_ver, source_commit,
717 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
726 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
718 maybe_unreachable=False):
727 maybe_unreachable=False):
719
728
720 commit_cache = collections.OrderedDict()
729 commit_cache = collections.OrderedDict()
721 missing_requirements = False
730 missing_requirements = False
722
731
723 try:
732 try:
724 pre_load = ["author", "date", "message", "branch", "parents"]
733 pre_load = ["author", "date", "message", "branch", "parents"]
725
734
726 pull_request_commits = pull_request_at_ver.revisions
735 pull_request_commits = pull_request_at_ver.revisions
727 log.debug('Loading %s commits from %s',
736 log.debug('Loading %s commits from %s',
728 len(pull_request_commits), commits_source_repo)
737 len(pull_request_commits), commits_source_repo)
729
738
730 for rev in pull_request_commits:
739 for rev in pull_request_commits:
731 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
740 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
732 maybe_unreachable=maybe_unreachable)
741 maybe_unreachable=maybe_unreachable)
733 commit_cache[comm.raw_id] = comm
742 commit_cache[comm.raw_id] = comm
734
743
735 # Order here matters, we first need to get target, and then
744 # Order here matters, we first need to get target, and then
736 # the source
745 # the source
737 target_commit = commits_source_repo.get_commit(
746 target_commit = commits_source_repo.get_commit(
738 commit_id=safe_str(target_ref_id))
747 commit_id=safe_str(target_ref_id))
739
748
740 source_commit = commits_source_repo.get_commit(
749 source_commit = commits_source_repo.get_commit(
741 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
750 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
742 except CommitDoesNotExistError:
751 except CommitDoesNotExistError:
743 log.warning('Failed to get commit from `{}` repo'.format(
752 log.warning('Failed to get commit from `{}` repo'.format(
744 commits_source_repo), exc_info=True)
753 commits_source_repo), exc_info=True)
745 except RepositoryRequirementError:
754 except RepositoryRequirementError:
746 log.warning('Failed to get all required data from repo', exc_info=True)
755 log.warning('Failed to get all required data from repo', exc_info=True)
747 missing_requirements = True
756 missing_requirements = True
748
757
749 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
758 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
750
759
751 try:
760 try:
752 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
761 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
753 except Exception:
762 except Exception:
754 ancestor_commit = None
763 ancestor_commit = None
755
764
756 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
765 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
757
766
758 def assure_not_empty_repo(self):
767 def assure_not_empty_repo(self):
759 _ = self.request.translate
768 _ = self.request.translate
760
769
761 try:
770 try:
762 self.db_repo.scm_instance().get_commit()
771 self.db_repo.scm_instance().get_commit()
763 except EmptyRepositoryError:
772 except EmptyRepositoryError:
764 h.flash(h.literal(_('There are no commits yet')),
773 h.flash(h.literal(_('There are no commits yet')),
765 category='warning')
774 category='warning')
766 raise HTTPFound(
775 raise HTTPFound(
767 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
776 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
768
777
769 @LoginRequired()
778 @LoginRequired()
770 @NotAnonymous()
779 @NotAnonymous()
771 @HasRepoPermissionAnyDecorator(
780 @HasRepoPermissionAnyDecorator(
772 'repository.read', 'repository.write', 'repository.admin')
781 'repository.read', 'repository.write', 'repository.admin')
773 @view_config(
782 @view_config(
774 route_name='pullrequest_new', request_method='GET',
783 route_name='pullrequest_new', request_method='GET',
775 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
784 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
776 def pull_request_new(self):
785 def pull_request_new(self):
777 _ = self.request.translate
786 _ = self.request.translate
778 c = self.load_default_context()
787 c = self.load_default_context()
779
788
780 self.assure_not_empty_repo()
789 self.assure_not_empty_repo()
781 source_repo = self.db_repo
790 source_repo = self.db_repo
782
791
783 commit_id = self.request.GET.get('commit')
792 commit_id = self.request.GET.get('commit')
784 branch_ref = self.request.GET.get('branch')
793 branch_ref = self.request.GET.get('branch')
785 bookmark_ref = self.request.GET.get('bookmark')
794 bookmark_ref = self.request.GET.get('bookmark')
786
795
787 try:
796 try:
788 source_repo_data = PullRequestModel().generate_repo_data(
797 source_repo_data = PullRequestModel().generate_repo_data(
789 source_repo, commit_id=commit_id,
798 source_repo, commit_id=commit_id,
790 branch=branch_ref, bookmark=bookmark_ref,
799 branch=branch_ref, bookmark=bookmark_ref,
791 translator=self.request.translate)
800 translator=self.request.translate)
792 except CommitDoesNotExistError as e:
801 except CommitDoesNotExistError as e:
793 log.exception(e)
802 log.exception(e)
794 h.flash(_('Commit does not exist'), 'error')
803 h.flash(_('Commit does not exist'), 'error')
795 raise HTTPFound(
804 raise HTTPFound(
796 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
805 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
797
806
798 default_target_repo = source_repo
807 default_target_repo = source_repo
799
808
800 if source_repo.parent and c.has_origin_repo_read_perm:
809 if source_repo.parent and c.has_origin_repo_read_perm:
801 parent_vcs_obj = source_repo.parent.scm_instance()
810 parent_vcs_obj = source_repo.parent.scm_instance()
802 if parent_vcs_obj and not parent_vcs_obj.is_empty():
811 if parent_vcs_obj and not parent_vcs_obj.is_empty():
803 # change default if we have a parent repo
812 # change default if we have a parent repo
804 default_target_repo = source_repo.parent
813 default_target_repo = source_repo.parent
805
814
806 target_repo_data = PullRequestModel().generate_repo_data(
815 target_repo_data = PullRequestModel().generate_repo_data(
807 default_target_repo, translator=self.request.translate)
816 default_target_repo, translator=self.request.translate)
808
817
809 selected_source_ref = source_repo_data['refs']['selected_ref']
818 selected_source_ref = source_repo_data['refs']['selected_ref']
810 title_source_ref = ''
819 title_source_ref = ''
811 if selected_source_ref:
820 if selected_source_ref:
812 title_source_ref = selected_source_ref.split(':', 2)[1]
821 title_source_ref = selected_source_ref.split(':', 2)[1]
813 c.default_title = PullRequestModel().generate_pullrequest_title(
822 c.default_title = PullRequestModel().generate_pullrequest_title(
814 source=source_repo.repo_name,
823 source=source_repo.repo_name,
815 source_ref=title_source_ref,
824 source_ref=title_source_ref,
816 target=default_target_repo.repo_name
825 target=default_target_repo.repo_name
817 )
826 )
818
827
819 c.default_repo_data = {
828 c.default_repo_data = {
820 'source_repo_name': source_repo.repo_name,
829 'source_repo_name': source_repo.repo_name,
821 'source_refs_json': json.dumps(source_repo_data),
830 'source_refs_json': json.dumps(source_repo_data),
822 'target_repo_name': default_target_repo.repo_name,
831 'target_repo_name': default_target_repo.repo_name,
823 'target_refs_json': json.dumps(target_repo_data),
832 'target_refs_json': json.dumps(target_repo_data),
824 }
833 }
825 c.default_source_ref = selected_source_ref
834 c.default_source_ref = selected_source_ref
826
835
827 return self._get_template_context(c)
836 return self._get_template_context(c)
828
837
829 @LoginRequired()
838 @LoginRequired()
830 @NotAnonymous()
839 @NotAnonymous()
831 @HasRepoPermissionAnyDecorator(
840 @HasRepoPermissionAnyDecorator(
832 'repository.read', 'repository.write', 'repository.admin')
841 'repository.read', 'repository.write', 'repository.admin')
833 @view_config(
842 @view_config(
834 route_name='pullrequest_repo_refs', request_method='GET',
843 route_name='pullrequest_repo_refs', request_method='GET',
835 renderer='json_ext', xhr=True)
844 renderer='json_ext', xhr=True)
836 def pull_request_repo_refs(self):
845 def pull_request_repo_refs(self):
837 self.load_default_context()
846 self.load_default_context()
838 target_repo_name = self.request.matchdict['target_repo_name']
847 target_repo_name = self.request.matchdict['target_repo_name']
839 repo = Repository.get_by_repo_name(target_repo_name)
848 repo = Repository.get_by_repo_name(target_repo_name)
840 if not repo:
849 if not repo:
841 raise HTTPNotFound()
850 raise HTTPNotFound()
842
851
843 target_perm = HasRepoPermissionAny(
852 target_perm = HasRepoPermissionAny(
844 'repository.read', 'repository.write', 'repository.admin')(
853 'repository.read', 'repository.write', 'repository.admin')(
845 target_repo_name)
854 target_repo_name)
846 if not target_perm:
855 if not target_perm:
847 raise HTTPNotFound()
856 raise HTTPNotFound()
848
857
849 return PullRequestModel().generate_repo_data(
858 return PullRequestModel().generate_repo_data(
850 repo, translator=self.request.translate)
859 repo, translator=self.request.translate)
851
860
852 @LoginRequired()
861 @LoginRequired()
853 @NotAnonymous()
862 @NotAnonymous()
854 @HasRepoPermissionAnyDecorator(
863 @HasRepoPermissionAnyDecorator(
855 'repository.read', 'repository.write', 'repository.admin')
864 'repository.read', 'repository.write', 'repository.admin')
856 @view_config(
865 @view_config(
857 route_name='pullrequest_repo_targets', request_method='GET',
866 route_name='pullrequest_repo_targets', request_method='GET',
858 renderer='json_ext', xhr=True)
867 renderer='json_ext', xhr=True)
859 def pullrequest_repo_targets(self):
868 def pullrequest_repo_targets(self):
860 _ = self.request.translate
869 _ = self.request.translate
861 filter_query = self.request.GET.get('query')
870 filter_query = self.request.GET.get('query')
862
871
863 # get the parents
872 # get the parents
864 parent_target_repos = []
873 parent_target_repos = []
865 if self.db_repo.parent:
874 if self.db_repo.parent:
866 parents_query = Repository.query() \
875 parents_query = Repository.query() \
867 .order_by(func.length(Repository.repo_name)) \
876 .order_by(func.length(Repository.repo_name)) \
868 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
877 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
869
878
870 if filter_query:
879 if filter_query:
871 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
880 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
872 parents_query = parents_query.filter(
881 parents_query = parents_query.filter(
873 Repository.repo_name.ilike(ilike_expression))
882 Repository.repo_name.ilike(ilike_expression))
874 parents = parents_query.limit(20).all()
883 parents = parents_query.limit(20).all()
875
884
876 for parent in parents:
885 for parent in parents:
877 parent_vcs_obj = parent.scm_instance()
886 parent_vcs_obj = parent.scm_instance()
878 if parent_vcs_obj and not parent_vcs_obj.is_empty():
887 if parent_vcs_obj and not parent_vcs_obj.is_empty():
879 parent_target_repos.append(parent)
888 parent_target_repos.append(parent)
880
889
881 # get other forks, and repo itself
890 # get other forks, and repo itself
882 query = Repository.query() \
891 query = Repository.query() \
883 .order_by(func.length(Repository.repo_name)) \
892 .order_by(func.length(Repository.repo_name)) \
884 .filter(
893 .filter(
885 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
894 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
886 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
895 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
887 ) \
896 ) \
888 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
897 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
889
898
890 if filter_query:
899 if filter_query:
891 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
900 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
892 query = query.filter(Repository.repo_name.ilike(ilike_expression))
901 query = query.filter(Repository.repo_name.ilike(ilike_expression))
893
902
894 limit = max(20 - len(parent_target_repos), 5) # not less then 5
903 limit = max(20 - len(parent_target_repos), 5) # not less then 5
895 target_repos = query.limit(limit).all()
904 target_repos = query.limit(limit).all()
896
905
897 all_target_repos = target_repos + parent_target_repos
906 all_target_repos = target_repos + parent_target_repos
898
907
899 repos = []
908 repos = []
900 # This checks permissions to the repositories
909 # This checks permissions to the repositories
901 for obj in ScmModel().get_repos(all_target_repos):
910 for obj in ScmModel().get_repos(all_target_repos):
902 repos.append({
911 repos.append({
903 'id': obj['name'],
912 'id': obj['name'],
904 'text': obj['name'],
913 'text': obj['name'],
905 'type': 'repo',
914 'type': 'repo',
906 'repo_id': obj['dbrepo']['repo_id'],
915 'repo_id': obj['dbrepo']['repo_id'],
907 'repo_type': obj['dbrepo']['repo_type'],
916 'repo_type': obj['dbrepo']['repo_type'],
908 'private': obj['dbrepo']['private'],
917 'private': obj['dbrepo']['private'],
909
918
910 })
919 })
911
920
912 data = {
921 data = {
913 'more': False,
922 'more': False,
914 'results': [{
923 'results': [{
915 'text': _('Repositories'),
924 'text': _('Repositories'),
916 'children': repos
925 'children': repos
917 }] if repos else []
926 }] if repos else []
918 }
927 }
919 return data
928 return data
920
929
921 @LoginRequired()
930 @LoginRequired()
922 @NotAnonymous()
931 @NotAnonymous()
923 @HasRepoPermissionAnyDecorator(
932 @HasRepoPermissionAnyDecorator(
924 'repository.read', 'repository.write', 'repository.admin')
933 'repository.read', 'repository.write', 'repository.admin')
925 @CSRFRequired()
934 @CSRFRequired()
926 @view_config(
935 @view_config(
927 route_name='pullrequest_create', request_method='POST',
936 route_name='pullrequest_create', request_method='POST',
928 renderer=None)
937 renderer=None)
929 def pull_request_create(self):
938 def pull_request_create(self):
930 _ = self.request.translate
939 _ = self.request.translate
931 self.assure_not_empty_repo()
940 self.assure_not_empty_repo()
932 self.load_default_context()
941 self.load_default_context()
933
942
934 controls = peppercorn.parse(self.request.POST.items())
943 controls = peppercorn.parse(self.request.POST.items())
935
944
936 try:
945 try:
937 form = PullRequestForm(
946 form = PullRequestForm(
938 self.request.translate, self.db_repo.repo_id)()
947 self.request.translate, self.db_repo.repo_id)()
939 _form = form.to_python(controls)
948 _form = form.to_python(controls)
940 except formencode.Invalid as errors:
949 except formencode.Invalid as errors:
941 if errors.error_dict.get('revisions'):
950 if errors.error_dict.get('revisions'):
942 msg = 'Revisions: %s' % errors.error_dict['revisions']
951 msg = 'Revisions: %s' % errors.error_dict['revisions']
943 elif errors.error_dict.get('pullrequest_title'):
952 elif errors.error_dict.get('pullrequest_title'):
944 msg = errors.error_dict.get('pullrequest_title')
953 msg = errors.error_dict.get('pullrequest_title')
945 else:
954 else:
946 msg = _('Error creating pull request: {}').format(errors)
955 msg = _('Error creating pull request: {}').format(errors)
947 log.exception(msg)
956 log.exception(msg)
948 h.flash(msg, 'error')
957 h.flash(msg, 'error')
949
958
950 # would rather just go back to form ...
959 # would rather just go back to form ...
951 raise HTTPFound(
960 raise HTTPFound(
952 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
961 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
953
962
954 source_repo = _form['source_repo']
963 source_repo = _form['source_repo']
955 source_ref = _form['source_ref']
964 source_ref = _form['source_ref']
956 target_repo = _form['target_repo']
965 target_repo = _form['target_repo']
957 target_ref = _form['target_ref']
966 target_ref = _form['target_ref']
958 commit_ids = _form['revisions'][::-1]
967 commit_ids = _form['revisions'][::-1]
959 common_ancestor_id = _form['common_ancestor']
968 common_ancestor_id = _form['common_ancestor']
960
969
961 # find the ancestor for this pr
970 # find the ancestor for this pr
962 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
971 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
963 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
972 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
964
973
965 if not (source_db_repo or target_db_repo):
974 if not (source_db_repo or target_db_repo):
966 h.flash(_('source_repo or target repo not found'), category='error')
975 h.flash(_('source_repo or target repo not found'), category='error')
967 raise HTTPFound(
976 raise HTTPFound(
968 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
977 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
969
978
970 # re-check permissions again here
979 # re-check permissions again here
971 # source_repo we must have read permissions
980 # source_repo we must have read permissions
972
981
973 source_perm = HasRepoPermissionAny(
982 source_perm = HasRepoPermissionAny(
974 'repository.read', 'repository.write', 'repository.admin')(
983 'repository.read', 'repository.write', 'repository.admin')(
975 source_db_repo.repo_name)
984 source_db_repo.repo_name)
976 if not source_perm:
985 if not source_perm:
977 msg = _('Not Enough permissions to source repo `{}`.'.format(
986 msg = _('Not Enough permissions to source repo `{}`.'.format(
978 source_db_repo.repo_name))
987 source_db_repo.repo_name))
979 h.flash(msg, category='error')
988 h.flash(msg, category='error')
980 # copy the args back to redirect
989 # copy the args back to redirect
981 org_query = self.request.GET.mixed()
990 org_query = self.request.GET.mixed()
982 raise HTTPFound(
991 raise HTTPFound(
983 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
992 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
984 _query=org_query))
993 _query=org_query))
985
994
986 # target repo we must have read permissions, and also later on
995 # target repo we must have read permissions, and also later on
987 # we want to check branch permissions here
996 # we want to check branch permissions here
988 target_perm = HasRepoPermissionAny(
997 target_perm = HasRepoPermissionAny(
989 'repository.read', 'repository.write', 'repository.admin')(
998 'repository.read', 'repository.write', 'repository.admin')(
990 target_db_repo.repo_name)
999 target_db_repo.repo_name)
991 if not target_perm:
1000 if not target_perm:
992 msg = _('Not Enough permissions to target repo `{}`.'.format(
1001 msg = _('Not Enough permissions to target repo `{}`.'.format(
993 target_db_repo.repo_name))
1002 target_db_repo.repo_name))
994 h.flash(msg, category='error')
1003 h.flash(msg, category='error')
995 # copy the args back to redirect
1004 # copy the args back to redirect
996 org_query = self.request.GET.mixed()
1005 org_query = self.request.GET.mixed()
997 raise HTTPFound(
1006 raise HTTPFound(
998 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1007 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
999 _query=org_query))
1008 _query=org_query))
1000
1009
1001 source_scm = source_db_repo.scm_instance()
1010 source_scm = source_db_repo.scm_instance()
1002 target_scm = target_db_repo.scm_instance()
1011 target_scm = target_db_repo.scm_instance()
1003
1012
1004 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1013 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1005 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1014 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1006
1015
1007 ancestor = source_scm.get_common_ancestor(
1016 ancestor = source_scm.get_common_ancestor(
1008 source_commit.raw_id, target_commit.raw_id, target_scm)
1017 source_commit.raw_id, target_commit.raw_id, target_scm)
1009
1018
1010 # recalculate target ref based on ancestor
1019 # recalculate target ref based on ancestor
1011 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1020 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1012 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1021 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1013
1022
1014 get_default_reviewers_data, validate_default_reviewers = \
1023 get_default_reviewers_data, validate_default_reviewers = \
1015 PullRequestModel().get_reviewer_functions()
1024 PullRequestModel().get_reviewer_functions()
1016
1025
1017 # recalculate reviewers logic, to make sure we can validate this
1026 # recalculate reviewers logic, to make sure we can validate this
1018 reviewer_rules = get_default_reviewers_data(
1027 reviewer_rules = get_default_reviewers_data(
1019 self._rhodecode_db_user, source_db_repo,
1028 self._rhodecode_db_user, source_db_repo,
1020 source_commit, target_db_repo, target_commit)
1029 source_commit, target_db_repo, target_commit)
1021
1030
1022 given_reviewers = _form['review_members']
1031 given_reviewers = _form['review_members']
1023 reviewers = validate_default_reviewers(
1032 reviewers = validate_default_reviewers(
1024 given_reviewers, reviewer_rules)
1033 given_reviewers, reviewer_rules)
1025
1034
1026 pullrequest_title = _form['pullrequest_title']
1035 pullrequest_title = _form['pullrequest_title']
1027 title_source_ref = source_ref.split(':', 2)[1]
1036 title_source_ref = source_ref.split(':', 2)[1]
1028 if not pullrequest_title:
1037 if not pullrequest_title:
1029 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1038 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1030 source=source_repo,
1039 source=source_repo,
1031 source_ref=title_source_ref,
1040 source_ref=title_source_ref,
1032 target=target_repo
1041 target=target_repo
1033 )
1042 )
1034
1043
1035 description = _form['pullrequest_desc']
1044 description = _form['pullrequest_desc']
1036 description_renderer = _form['description_renderer']
1045 description_renderer = _form['description_renderer']
1037
1046
1038 try:
1047 try:
1039 pull_request = PullRequestModel().create(
1048 pull_request = PullRequestModel().create(
1040 created_by=self._rhodecode_user.user_id,
1049 created_by=self._rhodecode_user.user_id,
1041 source_repo=source_repo,
1050 source_repo=source_repo,
1042 source_ref=source_ref,
1051 source_ref=source_ref,
1043 target_repo=target_repo,
1052 target_repo=target_repo,
1044 target_ref=target_ref,
1053 target_ref=target_ref,
1045 revisions=commit_ids,
1054 revisions=commit_ids,
1046 common_ancestor_id=common_ancestor_id,
1055 common_ancestor_id=common_ancestor_id,
1047 reviewers=reviewers,
1056 reviewers=reviewers,
1048 title=pullrequest_title,
1057 title=pullrequest_title,
1049 description=description,
1058 description=description,
1050 description_renderer=description_renderer,
1059 description_renderer=description_renderer,
1051 reviewer_data=reviewer_rules,
1060 reviewer_data=reviewer_rules,
1052 auth_user=self._rhodecode_user
1061 auth_user=self._rhodecode_user
1053 )
1062 )
1054 Session().commit()
1063 Session().commit()
1055
1064
1056 h.flash(_('Successfully opened new pull request'),
1065 h.flash(_('Successfully opened new pull request'),
1057 category='success')
1066 category='success')
1058 except Exception:
1067 except Exception:
1059 msg = _('Error occurred during creation of this pull request.')
1068 msg = _('Error occurred during creation of this pull request.')
1060 log.exception(msg)
1069 log.exception(msg)
1061 h.flash(msg, category='error')
1070 h.flash(msg, category='error')
1062
1071
1063 # copy the args back to redirect
1072 # copy the args back to redirect
1064 org_query = self.request.GET.mixed()
1073 org_query = self.request.GET.mixed()
1065 raise HTTPFound(
1074 raise HTTPFound(
1066 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1075 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1067 _query=org_query))
1076 _query=org_query))
1068
1077
1069 raise HTTPFound(
1078 raise HTTPFound(
1070 h.route_path('pullrequest_show', repo_name=target_repo,
1079 h.route_path('pullrequest_show', repo_name=target_repo,
1071 pull_request_id=pull_request.pull_request_id))
1080 pull_request_id=pull_request.pull_request_id))
1072
1081
1073 @LoginRequired()
1082 @LoginRequired()
1074 @NotAnonymous()
1083 @NotAnonymous()
1075 @HasRepoPermissionAnyDecorator(
1084 @HasRepoPermissionAnyDecorator(
1076 'repository.read', 'repository.write', 'repository.admin')
1085 'repository.read', 'repository.write', 'repository.admin')
1077 @CSRFRequired()
1086 @CSRFRequired()
1078 @view_config(
1087 @view_config(
1079 route_name='pullrequest_update', request_method='POST',
1088 route_name='pullrequest_update', request_method='POST',
1080 renderer='json_ext')
1089 renderer='json_ext')
1081 def pull_request_update(self):
1090 def pull_request_update(self):
1082 pull_request = PullRequest.get_or_404(
1091 pull_request = PullRequest.get_or_404(
1083 self.request.matchdict['pull_request_id'])
1092 self.request.matchdict['pull_request_id'])
1084 _ = self.request.translate
1093 _ = self.request.translate
1085
1094
1086 self.load_default_context()
1095 self.load_default_context()
1087 redirect_url = None
1096 redirect_url = None
1088
1097
1089 if pull_request.is_closed():
1098 if pull_request.is_closed():
1090 log.debug('update: forbidden because pull request is closed')
1099 log.debug('update: forbidden because pull request is closed')
1091 msg = _(u'Cannot update closed pull requests.')
1100 msg = _(u'Cannot update closed pull requests.')
1092 h.flash(msg, category='error')
1101 h.flash(msg, category='error')
1093 return {'response': True,
1102 return {'response': True,
1094 'redirect_url': redirect_url}
1103 'redirect_url': redirect_url}
1095
1104
1096 is_state_changing = pull_request.is_state_changing()
1105 is_state_changing = pull_request.is_state_changing()
1097
1106
1098 # only owner or admin can update it
1107 # only owner or admin can update it
1099 allowed_to_update = PullRequestModel().check_user_update(
1108 allowed_to_update = PullRequestModel().check_user_update(
1100 pull_request, self._rhodecode_user)
1109 pull_request, self._rhodecode_user)
1101 if allowed_to_update:
1110 if allowed_to_update:
1102 controls = peppercorn.parse(self.request.POST.items())
1111 controls = peppercorn.parse(self.request.POST.items())
1103 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1112 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1104
1113
1105 if 'review_members' in controls:
1114 if 'review_members' in controls:
1106 self._update_reviewers(
1115 self._update_reviewers(
1107 pull_request, controls['review_members'],
1116 pull_request, controls['review_members'],
1108 pull_request.reviewer_data)
1117 pull_request.reviewer_data)
1109 elif str2bool(self.request.POST.get('update_commits', 'false')):
1118 elif str2bool(self.request.POST.get('update_commits', 'false')):
1110 if is_state_changing:
1119 if is_state_changing:
1111 log.debug('commits update: forbidden because pull request is in state %s',
1120 log.debug('commits update: forbidden because pull request is in state %s',
1112 pull_request.pull_request_state)
1121 pull_request.pull_request_state)
1113 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1122 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1114 u'Current state is: `{}`').format(
1123 u'Current state is: `{}`').format(
1115 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1124 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1116 h.flash(msg, category='error')
1125 h.flash(msg, category='error')
1117 return {'response': True,
1126 return {'response': True,
1118 'redirect_url': redirect_url}
1127 'redirect_url': redirect_url}
1119
1128
1120 self._update_commits(pull_request)
1129 self._update_commits(pull_request)
1121 if force_refresh:
1130 if force_refresh:
1122 redirect_url = h.route_path(
1131 redirect_url = h.route_path(
1123 'pullrequest_show', repo_name=self.db_repo_name,
1132 'pullrequest_show', repo_name=self.db_repo_name,
1124 pull_request_id=pull_request.pull_request_id,
1133 pull_request_id=pull_request.pull_request_id,
1125 _query={"force_refresh": 1})
1134 _query={"force_refresh": 1})
1126 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1135 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1127 self._edit_pull_request(pull_request)
1136 self._edit_pull_request(pull_request)
1128 else:
1137 else:
1129 raise HTTPBadRequest()
1138 raise HTTPBadRequest()
1130
1139
1131 return {'response': True,
1140 return {'response': True,
1132 'redirect_url': redirect_url}
1141 'redirect_url': redirect_url}
1133 raise HTTPForbidden()
1142 raise HTTPForbidden()
1134
1143
1135 def _edit_pull_request(self, pull_request):
1144 def _edit_pull_request(self, pull_request):
1136 _ = self.request.translate
1145 _ = self.request.translate
1137
1146
1138 try:
1147 try:
1139 PullRequestModel().edit(
1148 PullRequestModel().edit(
1140 pull_request,
1149 pull_request,
1141 self.request.POST.get('title'),
1150 self.request.POST.get('title'),
1142 self.request.POST.get('description'),
1151 self.request.POST.get('description'),
1143 self.request.POST.get('description_renderer'),
1152 self.request.POST.get('description_renderer'),
1144 self._rhodecode_user)
1153 self._rhodecode_user)
1145 except ValueError:
1154 except ValueError:
1146 msg = _(u'Cannot update closed pull requests.')
1155 msg = _(u'Cannot update closed pull requests.')
1147 h.flash(msg, category='error')
1156 h.flash(msg, category='error')
1148 return
1157 return
1149 else:
1158 else:
1150 Session().commit()
1159 Session().commit()
1151
1160
1152 msg = _(u'Pull request title & description updated.')
1161 msg = _(u'Pull request title & description updated.')
1153 h.flash(msg, category='success')
1162 h.flash(msg, category='success')
1154 return
1163 return
1155
1164
1156 def _update_commits(self, pull_request):
1165 def _update_commits(self, pull_request):
1157 _ = self.request.translate
1166 _ = self.request.translate
1158
1167
1159 with pull_request.set_state(PullRequest.STATE_UPDATING):
1168 with pull_request.set_state(PullRequest.STATE_UPDATING):
1160 resp = PullRequestModel().update_commits(
1169 resp = PullRequestModel().update_commits(
1161 pull_request, self._rhodecode_db_user)
1170 pull_request, self._rhodecode_db_user)
1162
1171
1163 if resp.executed:
1172 if resp.executed:
1164
1173
1165 if resp.target_changed and resp.source_changed:
1174 if resp.target_changed and resp.source_changed:
1166 changed = 'target and source repositories'
1175 changed = 'target and source repositories'
1167 elif resp.target_changed and not resp.source_changed:
1176 elif resp.target_changed and not resp.source_changed:
1168 changed = 'target repository'
1177 changed = 'target repository'
1169 elif not resp.target_changed and resp.source_changed:
1178 elif not resp.target_changed and resp.source_changed:
1170 changed = 'source repository'
1179 changed = 'source repository'
1171 else:
1180 else:
1172 changed = 'nothing'
1181 changed = 'nothing'
1173
1182
1174 msg = _(u'Pull request updated to "{source_commit_id}" with '
1183 msg = _(u'Pull request updated to "{source_commit_id}" with '
1175 u'{count_added} added, {count_removed} removed commits. '
1184 u'{count_added} added, {count_removed} removed commits. '
1176 u'Source of changes: {change_source}')
1185 u'Source of changes: {change_source}')
1177 msg = msg.format(
1186 msg = msg.format(
1178 source_commit_id=pull_request.source_ref_parts.commit_id,
1187 source_commit_id=pull_request.source_ref_parts.commit_id,
1179 count_added=len(resp.changes.added),
1188 count_added=len(resp.changes.added),
1180 count_removed=len(resp.changes.removed),
1189 count_removed=len(resp.changes.removed),
1181 change_source=changed)
1190 change_source=changed)
1182 h.flash(msg, category='success')
1191 h.flash(msg, category='success')
1183
1192
1184 channel = '/repo${}$/pr/{}'.format(
1193 channel = '/repo${}$/pr/{}'.format(
1185 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1194 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1186 message = msg + (
1195 message = msg + (
1187 ' - <a onclick="window.location.reload()">'
1196 ' - <a onclick="window.location.reload()">'
1188 '<strong>{}</strong></a>'.format(_('Reload page')))
1197 '<strong>{}</strong></a>'.format(_('Reload page')))
1189 channelstream.post_message(
1198 channelstream.post_message(
1190 channel, message, self._rhodecode_user.username,
1199 channel, message, self._rhodecode_user.username,
1191 registry=self.request.registry)
1200 registry=self.request.registry)
1192 else:
1201 else:
1193 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1202 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1194 warning_reasons = [
1203 warning_reasons = [
1195 UpdateFailureReason.NO_CHANGE,
1204 UpdateFailureReason.NO_CHANGE,
1196 UpdateFailureReason.WRONG_REF_TYPE,
1205 UpdateFailureReason.WRONG_REF_TYPE,
1197 ]
1206 ]
1198 category = 'warning' if resp.reason in warning_reasons else 'error'
1207 category = 'warning' if resp.reason in warning_reasons else 'error'
1199 h.flash(msg, category=category)
1208 h.flash(msg, category=category)
1200
1209
1201 @LoginRequired()
1210 @LoginRequired()
1202 @NotAnonymous()
1211 @NotAnonymous()
1203 @HasRepoPermissionAnyDecorator(
1212 @HasRepoPermissionAnyDecorator(
1204 'repository.read', 'repository.write', 'repository.admin')
1213 'repository.read', 'repository.write', 'repository.admin')
1205 @CSRFRequired()
1214 @CSRFRequired()
1206 @view_config(
1215 @view_config(
1207 route_name='pullrequest_merge', request_method='POST',
1216 route_name='pullrequest_merge', request_method='POST',
1208 renderer='json_ext')
1217 renderer='json_ext')
1209 def pull_request_merge(self):
1218 def pull_request_merge(self):
1210 """
1219 """
1211 Merge will perform a server-side merge of the specified
1220 Merge will perform a server-side merge of the specified
1212 pull request, if the pull request is approved and mergeable.
1221 pull request, if the pull request is approved and mergeable.
1213 After successful merging, the pull request is automatically
1222 After successful merging, the pull request is automatically
1214 closed, with a relevant comment.
1223 closed, with a relevant comment.
1215 """
1224 """
1216 pull_request = PullRequest.get_or_404(
1225 pull_request = PullRequest.get_or_404(
1217 self.request.matchdict['pull_request_id'])
1226 self.request.matchdict['pull_request_id'])
1218 _ = self.request.translate
1227 _ = self.request.translate
1219
1228
1220 if pull_request.is_state_changing():
1229 if pull_request.is_state_changing():
1221 log.debug('show: forbidden because pull request is in state %s',
1230 log.debug('show: forbidden because pull request is in state %s',
1222 pull_request.pull_request_state)
1231 pull_request.pull_request_state)
1223 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1232 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1224 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1233 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1225 pull_request.pull_request_state)
1234 pull_request.pull_request_state)
1226 h.flash(msg, category='error')
1235 h.flash(msg, category='error')
1227 raise HTTPFound(
1236 raise HTTPFound(
1228 h.route_path('pullrequest_show',
1237 h.route_path('pullrequest_show',
1229 repo_name=pull_request.target_repo.repo_name,
1238 repo_name=pull_request.target_repo.repo_name,
1230 pull_request_id=pull_request.pull_request_id))
1239 pull_request_id=pull_request.pull_request_id))
1231
1240
1232 self.load_default_context()
1241 self.load_default_context()
1233
1242
1234 with pull_request.set_state(PullRequest.STATE_UPDATING):
1243 with pull_request.set_state(PullRequest.STATE_UPDATING):
1235 check = MergeCheck.validate(
1244 check = MergeCheck.validate(
1236 pull_request, auth_user=self._rhodecode_user,
1245 pull_request, auth_user=self._rhodecode_user,
1237 translator=self.request.translate)
1246 translator=self.request.translate)
1238 merge_possible = not check.failed
1247 merge_possible = not check.failed
1239
1248
1240 for err_type, error_msg in check.errors:
1249 for err_type, error_msg in check.errors:
1241 h.flash(error_msg, category=err_type)
1250 h.flash(error_msg, category=err_type)
1242
1251
1243 if merge_possible:
1252 if merge_possible:
1244 log.debug("Pre-conditions checked, trying to merge.")
1253 log.debug("Pre-conditions checked, trying to merge.")
1245 extras = vcs_operation_context(
1254 extras = vcs_operation_context(
1246 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1255 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1247 username=self._rhodecode_db_user.username, action='push',
1256 username=self._rhodecode_db_user.username, action='push',
1248 scm=pull_request.target_repo.repo_type)
1257 scm=pull_request.target_repo.repo_type)
1249 with pull_request.set_state(PullRequest.STATE_UPDATING):
1258 with pull_request.set_state(PullRequest.STATE_UPDATING):
1250 self._merge_pull_request(
1259 self._merge_pull_request(
1251 pull_request, self._rhodecode_db_user, extras)
1260 pull_request, self._rhodecode_db_user, extras)
1252 else:
1261 else:
1253 log.debug("Pre-conditions failed, NOT merging.")
1262 log.debug("Pre-conditions failed, NOT merging.")
1254
1263
1255 raise HTTPFound(
1264 raise HTTPFound(
1256 h.route_path('pullrequest_show',
1265 h.route_path('pullrequest_show',
1257 repo_name=pull_request.target_repo.repo_name,
1266 repo_name=pull_request.target_repo.repo_name,
1258 pull_request_id=pull_request.pull_request_id))
1267 pull_request_id=pull_request.pull_request_id))
1259
1268
1260 def _merge_pull_request(self, pull_request, user, extras):
1269 def _merge_pull_request(self, pull_request, user, extras):
1261 _ = self.request.translate
1270 _ = self.request.translate
1262 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1271 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1263
1272
1264 if merge_resp.executed:
1273 if merge_resp.executed:
1265 log.debug("The merge was successful, closing the pull request.")
1274 log.debug("The merge was successful, closing the pull request.")
1266 PullRequestModel().close_pull_request(
1275 PullRequestModel().close_pull_request(
1267 pull_request.pull_request_id, user)
1276 pull_request.pull_request_id, user)
1268 Session().commit()
1277 Session().commit()
1269 msg = _('Pull request was successfully merged and closed.')
1278 msg = _('Pull request was successfully merged and closed.')
1270 h.flash(msg, category='success')
1279 h.flash(msg, category='success')
1271 else:
1280 else:
1272 log.debug(
1281 log.debug(
1273 "The merge was not successful. Merge response: %s", merge_resp)
1282 "The merge was not successful. Merge response: %s", merge_resp)
1274 msg = merge_resp.merge_status_message
1283 msg = merge_resp.merge_status_message
1275 h.flash(msg, category='error')
1284 h.flash(msg, category='error')
1276
1285
1277 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1286 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1278 _ = self.request.translate
1287 _ = self.request.translate
1279
1288
1280 get_default_reviewers_data, validate_default_reviewers = \
1289 get_default_reviewers_data, validate_default_reviewers = \
1281 PullRequestModel().get_reviewer_functions()
1290 PullRequestModel().get_reviewer_functions()
1282
1291
1283 try:
1292 try:
1284 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1293 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1285 except ValueError as e:
1294 except ValueError as e:
1286 log.error('Reviewers Validation: {}'.format(e))
1295 log.error('Reviewers Validation: {}'.format(e))
1287 h.flash(e, category='error')
1296 h.flash(e, category='error')
1288 return
1297 return
1289
1298
1290 old_calculated_status = pull_request.calculated_review_status()
1299 old_calculated_status = pull_request.calculated_review_status()
1291 PullRequestModel().update_reviewers(
1300 PullRequestModel().update_reviewers(
1292 pull_request, reviewers, self._rhodecode_user)
1301 pull_request, reviewers, self._rhodecode_user)
1293 h.flash(_('Pull request reviewers updated.'), category='success')
1302 h.flash(_('Pull request reviewers updated.'), category='success')
1294 Session().commit()
1303 Session().commit()
1295
1304
1296 # trigger status changed if change in reviewers changes the status
1305 # trigger status changed if change in reviewers changes the status
1297 calculated_status = pull_request.calculated_review_status()
1306 calculated_status = pull_request.calculated_review_status()
1298 if old_calculated_status != calculated_status:
1307 if old_calculated_status != calculated_status:
1299 PullRequestModel().trigger_pull_request_hook(
1308 PullRequestModel().trigger_pull_request_hook(
1300 pull_request, self._rhodecode_user, 'review_status_change',
1309 pull_request, self._rhodecode_user, 'review_status_change',
1301 data={'status': calculated_status})
1310 data={'status': calculated_status})
1302
1311
1303 @LoginRequired()
1312 @LoginRequired()
1304 @NotAnonymous()
1313 @NotAnonymous()
1305 @HasRepoPermissionAnyDecorator(
1314 @HasRepoPermissionAnyDecorator(
1306 'repository.read', 'repository.write', 'repository.admin')
1315 'repository.read', 'repository.write', 'repository.admin')
1307 @CSRFRequired()
1316 @CSRFRequired()
1308 @view_config(
1317 @view_config(
1309 route_name='pullrequest_delete', request_method='POST',
1318 route_name='pullrequest_delete', request_method='POST',
1310 renderer='json_ext')
1319 renderer='json_ext')
1311 def pull_request_delete(self):
1320 def pull_request_delete(self):
1312 _ = self.request.translate
1321 _ = self.request.translate
1313
1322
1314 pull_request = PullRequest.get_or_404(
1323 pull_request = PullRequest.get_or_404(
1315 self.request.matchdict['pull_request_id'])
1324 self.request.matchdict['pull_request_id'])
1316 self.load_default_context()
1325 self.load_default_context()
1317
1326
1318 pr_closed = pull_request.is_closed()
1327 pr_closed = pull_request.is_closed()
1319 allowed_to_delete = PullRequestModel().check_user_delete(
1328 allowed_to_delete = PullRequestModel().check_user_delete(
1320 pull_request, self._rhodecode_user) and not pr_closed
1329 pull_request, self._rhodecode_user) and not pr_closed
1321
1330
1322 # only owner can delete it !
1331 # only owner can delete it !
1323 if allowed_to_delete:
1332 if allowed_to_delete:
1324 PullRequestModel().delete(pull_request, self._rhodecode_user)
1333 PullRequestModel().delete(pull_request, self._rhodecode_user)
1325 Session().commit()
1334 Session().commit()
1326 h.flash(_('Successfully deleted pull request'),
1335 h.flash(_('Successfully deleted pull request'),
1327 category='success')
1336 category='success')
1328 raise HTTPFound(h.route_path('pullrequest_show_all',
1337 raise HTTPFound(h.route_path('pullrequest_show_all',
1329 repo_name=self.db_repo_name))
1338 repo_name=self.db_repo_name))
1330
1339
1331 log.warning('user %s tried to delete pull request without access',
1340 log.warning('user %s tried to delete pull request without access',
1332 self._rhodecode_user)
1341 self._rhodecode_user)
1333 raise HTTPNotFound()
1342 raise HTTPNotFound()
1334
1343
1335 @LoginRequired()
1344 @LoginRequired()
1336 @NotAnonymous()
1345 @NotAnonymous()
1337 @HasRepoPermissionAnyDecorator(
1346 @HasRepoPermissionAnyDecorator(
1338 'repository.read', 'repository.write', 'repository.admin')
1347 'repository.read', 'repository.write', 'repository.admin')
1339 @CSRFRequired()
1348 @CSRFRequired()
1340 @view_config(
1349 @view_config(
1341 route_name='pullrequest_comment_create', request_method='POST',
1350 route_name='pullrequest_comment_create', request_method='POST',
1342 renderer='json_ext')
1351 renderer='json_ext')
1343 def pull_request_comment_create(self):
1352 def pull_request_comment_create(self):
1344 _ = self.request.translate
1353 _ = self.request.translate
1345
1354
1346 pull_request = PullRequest.get_or_404(
1355 pull_request = PullRequest.get_or_404(
1347 self.request.matchdict['pull_request_id'])
1356 self.request.matchdict['pull_request_id'])
1348 pull_request_id = pull_request.pull_request_id
1357 pull_request_id = pull_request.pull_request_id
1349
1358
1350 if pull_request.is_closed():
1359 if pull_request.is_closed():
1351 log.debug('comment: forbidden because pull request is closed')
1360 log.debug('comment: forbidden because pull request is closed')
1352 raise HTTPForbidden()
1361 raise HTTPForbidden()
1353
1362
1354 allowed_to_comment = PullRequestModel().check_user_comment(
1363 allowed_to_comment = PullRequestModel().check_user_comment(
1355 pull_request, self._rhodecode_user)
1364 pull_request, self._rhodecode_user)
1356 if not allowed_to_comment:
1365 if not allowed_to_comment:
1357 log.debug(
1366 log.debug(
1358 'comment: forbidden because pull request is from forbidden repo')
1367 'comment: forbidden because pull request is from forbidden repo')
1359 raise HTTPForbidden()
1368 raise HTTPForbidden()
1360
1369
1361 c = self.load_default_context()
1370 c = self.load_default_context()
1362
1371
1363 status = self.request.POST.get('changeset_status', None)
1372 status = self.request.POST.get('changeset_status', None)
1364 text = self.request.POST.get('text')
1373 text = self.request.POST.get('text')
1365 comment_type = self.request.POST.get('comment_type')
1374 comment_type = self.request.POST.get('comment_type')
1366 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1375 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1367 close_pull_request = self.request.POST.get('close_pull_request')
1376 close_pull_request = self.request.POST.get('close_pull_request')
1368
1377
1369 # the logic here should work like following, if we submit close
1378 # the logic here should work like following, if we submit close
1370 # pr comment, use `close_pull_request_with_comment` function
1379 # pr comment, use `close_pull_request_with_comment` function
1371 # else handle regular comment logic
1380 # else handle regular comment logic
1372
1381
1373 if close_pull_request:
1382 if close_pull_request:
1374 # only owner or admin or person with write permissions
1383 # only owner or admin or person with write permissions
1375 allowed_to_close = PullRequestModel().check_user_update(
1384 allowed_to_close = PullRequestModel().check_user_update(
1376 pull_request, self._rhodecode_user)
1385 pull_request, self._rhodecode_user)
1377 if not allowed_to_close:
1386 if not allowed_to_close:
1378 log.debug('comment: forbidden because not allowed to close '
1387 log.debug('comment: forbidden because not allowed to close '
1379 'pull request %s', pull_request_id)
1388 'pull request %s', pull_request_id)
1380 raise HTTPForbidden()
1389 raise HTTPForbidden()
1381
1390
1382 # This also triggers `review_status_change`
1391 # This also triggers `review_status_change`
1383 comment, status = PullRequestModel().close_pull_request_with_comment(
1392 comment, status = PullRequestModel().close_pull_request_with_comment(
1384 pull_request, self._rhodecode_user, self.db_repo, message=text,
1393 pull_request, self._rhodecode_user, self.db_repo, message=text,
1385 auth_user=self._rhodecode_user)
1394 auth_user=self._rhodecode_user)
1386 Session().flush()
1395 Session().flush()
1387
1396
1388 PullRequestModel().trigger_pull_request_hook(
1397 PullRequestModel().trigger_pull_request_hook(
1389 pull_request, self._rhodecode_user, 'comment',
1398 pull_request, self._rhodecode_user, 'comment',
1390 data={'comment': comment})
1399 data={'comment': comment})
1391
1400
1392 else:
1401 else:
1393 # regular comment case, could be inline, or one with status.
1402 # regular comment case, could be inline, or one with status.
1394 # for that one we check also permissions
1403 # for that one we check also permissions
1395
1404
1396 allowed_to_change_status = PullRequestModel().check_user_change_status(
1405 allowed_to_change_status = PullRequestModel().check_user_change_status(
1397 pull_request, self._rhodecode_user)
1406 pull_request, self._rhodecode_user)
1398
1407
1399 if status and allowed_to_change_status:
1408 if status and allowed_to_change_status:
1400 message = (_('Status change %(transition_icon)s %(status)s')
1409 message = (_('Status change %(transition_icon)s %(status)s')
1401 % {'transition_icon': '>',
1410 % {'transition_icon': '>',
1402 'status': ChangesetStatus.get_status_lbl(status)})
1411 'status': ChangesetStatus.get_status_lbl(status)})
1403 text = text or message
1412 text = text or message
1404
1413
1405 comment = CommentsModel().create(
1414 comment = CommentsModel().create(
1406 text=text,
1415 text=text,
1407 repo=self.db_repo.repo_id,
1416 repo=self.db_repo.repo_id,
1408 user=self._rhodecode_user.user_id,
1417 user=self._rhodecode_user.user_id,
1409 pull_request=pull_request,
1418 pull_request=pull_request,
1410 f_path=self.request.POST.get('f_path'),
1419 f_path=self.request.POST.get('f_path'),
1411 line_no=self.request.POST.get('line'),
1420 line_no=self.request.POST.get('line'),
1412 status_change=(ChangesetStatus.get_status_lbl(status)
1421 status_change=(ChangesetStatus.get_status_lbl(status)
1413 if status and allowed_to_change_status else None),
1422 if status and allowed_to_change_status else None),
1414 status_change_type=(status
1423 status_change_type=(status
1415 if status and allowed_to_change_status else None),
1424 if status and allowed_to_change_status else None),
1416 comment_type=comment_type,
1425 comment_type=comment_type,
1417 resolves_comment_id=resolves_comment_id,
1426 resolves_comment_id=resolves_comment_id,
1418 auth_user=self._rhodecode_user
1427 auth_user=self._rhodecode_user
1419 )
1428 )
1420
1429
1421 if allowed_to_change_status:
1430 if allowed_to_change_status:
1422 # calculate old status before we change it
1431 # calculate old status before we change it
1423 old_calculated_status = pull_request.calculated_review_status()
1432 old_calculated_status = pull_request.calculated_review_status()
1424
1433
1425 # get status if set !
1434 # get status if set !
1426 if status:
1435 if status:
1427 ChangesetStatusModel().set_status(
1436 ChangesetStatusModel().set_status(
1428 self.db_repo.repo_id,
1437 self.db_repo.repo_id,
1429 status,
1438 status,
1430 self._rhodecode_user.user_id,
1439 self._rhodecode_user.user_id,
1431 comment,
1440 comment,
1432 pull_request=pull_request
1441 pull_request=pull_request
1433 )
1442 )
1434
1443
1435 Session().flush()
1444 Session().flush()
1436 # this is somehow required to get access to some relationship
1445 # this is somehow required to get access to some relationship
1437 # loaded on comment
1446 # loaded on comment
1438 Session().refresh(comment)
1447 Session().refresh(comment)
1439
1448
1440 PullRequestModel().trigger_pull_request_hook(
1449 PullRequestModel().trigger_pull_request_hook(
1441 pull_request, self._rhodecode_user, 'comment',
1450 pull_request, self._rhodecode_user, 'comment',
1442 data={'comment': comment})
1451 data={'comment': comment})
1443
1452
1444 # we now calculate the status of pull request, and based on that
1453 # we now calculate the status of pull request, and based on that
1445 # calculation we set the commits status
1454 # calculation we set the commits status
1446 calculated_status = pull_request.calculated_review_status()
1455 calculated_status = pull_request.calculated_review_status()
1447 if old_calculated_status != calculated_status:
1456 if old_calculated_status != calculated_status:
1448 PullRequestModel().trigger_pull_request_hook(
1457 PullRequestModel().trigger_pull_request_hook(
1449 pull_request, self._rhodecode_user, 'review_status_change',
1458 pull_request, self._rhodecode_user, 'review_status_change',
1450 data={'status': calculated_status})
1459 data={'status': calculated_status})
1451
1460
1452 Session().commit()
1461 Session().commit()
1453
1462
1454 data = {
1463 data = {
1455 'target_id': h.safeid(h.safe_unicode(
1464 'target_id': h.safeid(h.safe_unicode(
1456 self.request.POST.get('f_path'))),
1465 self.request.POST.get('f_path'))),
1457 }
1466 }
1458 if comment:
1467 if comment:
1459 c.co = comment
1468 c.co = comment
1460 rendered_comment = render(
1469 rendered_comment = render(
1461 'rhodecode:templates/changeset/changeset_comment_block.mako',
1470 'rhodecode:templates/changeset/changeset_comment_block.mako',
1462 self._get_template_context(c), self.request)
1471 self._get_template_context(c), self.request)
1463
1472
1464 data.update(comment.get_dict())
1473 data.update(comment.get_dict())
1465 data.update({'rendered_text': rendered_comment})
1474 data.update({'rendered_text': rendered_comment})
1466
1475
1467 return data
1476 return data
1468
1477
1469 @LoginRequired()
1478 @LoginRequired()
1470 @NotAnonymous()
1479 @NotAnonymous()
1471 @HasRepoPermissionAnyDecorator(
1480 @HasRepoPermissionAnyDecorator(
1472 'repository.read', 'repository.write', 'repository.admin')
1481 'repository.read', 'repository.write', 'repository.admin')
1473 @CSRFRequired()
1482 @CSRFRequired()
1474 @view_config(
1483 @view_config(
1475 route_name='pullrequest_comment_delete', request_method='POST',
1484 route_name='pullrequest_comment_delete', request_method='POST',
1476 renderer='json_ext')
1485 renderer='json_ext')
1477 def pull_request_comment_delete(self):
1486 def pull_request_comment_delete(self):
1478 pull_request = PullRequest.get_or_404(
1487 pull_request = PullRequest.get_or_404(
1479 self.request.matchdict['pull_request_id'])
1488 self.request.matchdict['pull_request_id'])
1480
1489
1481 comment = ChangesetComment.get_or_404(
1490 comment = ChangesetComment.get_or_404(
1482 self.request.matchdict['comment_id'])
1491 self.request.matchdict['comment_id'])
1483 comment_id = comment.comment_id
1492 comment_id = comment.comment_id
1484
1493
1485 if comment.immutable:
1494 if comment.immutable:
1486 # don't allow deleting comments that are immutable
1495 # don't allow deleting comments that are immutable
1487 raise HTTPForbidden()
1496 raise HTTPForbidden()
1488
1497
1489 if pull_request.is_closed():
1498 if pull_request.is_closed():
1490 log.debug('comment: forbidden because pull request is closed')
1499 log.debug('comment: forbidden because pull request is closed')
1491 raise HTTPForbidden()
1500 raise HTTPForbidden()
1492
1501
1493 if not comment:
1502 if not comment:
1494 log.debug('Comment with id:%s not found, skipping', comment_id)
1503 log.debug('Comment with id:%s not found, skipping', comment_id)
1495 # comment already deleted in another call probably
1504 # comment already deleted in another call probably
1496 return True
1505 return True
1497
1506
1498 if comment.pull_request.is_closed():
1507 if comment.pull_request.is_closed():
1499 # don't allow deleting comments on closed pull request
1508 # don't allow deleting comments on closed pull request
1500 raise HTTPForbidden()
1509 raise HTTPForbidden()
1501
1510
1502 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1511 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1503 super_admin = h.HasPermissionAny('hg.admin')()
1512 super_admin = h.HasPermissionAny('hg.admin')()
1504 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1513 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1505 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1514 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1506 comment_repo_admin = is_repo_admin and is_repo_comment
1515 comment_repo_admin = is_repo_admin and is_repo_comment
1507
1516
1508 if super_admin or comment_owner or comment_repo_admin:
1517 if super_admin or comment_owner or comment_repo_admin:
1509 old_calculated_status = comment.pull_request.calculated_review_status()
1518 old_calculated_status = comment.pull_request.calculated_review_status()
1510 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1519 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1511 Session().commit()
1520 Session().commit()
1512 calculated_status = comment.pull_request.calculated_review_status()
1521 calculated_status = comment.pull_request.calculated_review_status()
1513 if old_calculated_status != calculated_status:
1522 if old_calculated_status != calculated_status:
1514 PullRequestModel().trigger_pull_request_hook(
1523 PullRequestModel().trigger_pull_request_hook(
1515 comment.pull_request, self._rhodecode_user, 'review_status_change',
1524 comment.pull_request, self._rhodecode_user, 'review_status_change',
1516 data={'status': calculated_status})
1525 data={'status': calculated_status})
1517 return True
1526 return True
1518 else:
1527 else:
1519 log.warning('No permissions for user %s to delete comment_id: %s',
1528 log.warning('No permissions for user %s to delete comment_id: %s',
1520 self._rhodecode_db_user, comment_id)
1529 self._rhodecode_db_user, comment_id)
1521 raise HTTPNotFound()
1530 raise HTTPNotFound()
1522
1531
1523 @LoginRequired()
1532 @LoginRequired()
1524 @NotAnonymous()
1533 @NotAnonymous()
1525 @HasRepoPermissionAnyDecorator(
1534 @HasRepoPermissionAnyDecorator(
1526 'repository.read', 'repository.write', 'repository.admin')
1535 'repository.read', 'repository.write', 'repository.admin')
1527 @CSRFRequired()
1536 @CSRFRequired()
1528 @view_config(
1537 @view_config(
1529 route_name='pullrequest_comment_edit', request_method='POST',
1538 route_name='pullrequest_comment_edit', request_method='POST',
1530 renderer='json_ext')
1539 renderer='json_ext')
1531 def pull_request_comment_edit(self):
1540 def pull_request_comment_edit(self):
1532 self.load_default_context()
1541 self.load_default_context()
1533
1542
1534 pull_request = PullRequest.get_or_404(
1543 pull_request = PullRequest.get_or_404(
1535 self.request.matchdict['pull_request_id']
1544 self.request.matchdict['pull_request_id']
1536 )
1545 )
1537 comment = ChangesetComment.get_or_404(
1546 comment = ChangesetComment.get_or_404(
1538 self.request.matchdict['comment_id']
1547 self.request.matchdict['comment_id']
1539 )
1548 )
1540 comment_id = comment.comment_id
1549 comment_id = comment.comment_id
1541
1550
1542 if comment.immutable:
1551 if comment.immutable:
1543 # don't allow deleting comments that are immutable
1552 # don't allow deleting comments that are immutable
1544 raise HTTPForbidden()
1553 raise HTTPForbidden()
1545
1554
1546 if pull_request.is_closed():
1555 if pull_request.is_closed():
1547 log.debug('comment: forbidden because pull request is closed')
1556 log.debug('comment: forbidden because pull request is closed')
1548 raise HTTPForbidden()
1557 raise HTTPForbidden()
1549
1558
1550 if not comment:
1559 if not comment:
1551 log.debug('Comment with id:%s not found, skipping', comment_id)
1560 log.debug('Comment with id:%s not found, skipping', comment_id)
1552 # comment already deleted in another call probably
1561 # comment already deleted in another call probably
1553 return True
1562 return True
1554
1563
1555 if comment.pull_request.is_closed():
1564 if comment.pull_request.is_closed():
1556 # don't allow deleting comments on closed pull request
1565 # don't allow deleting comments on closed pull request
1557 raise HTTPForbidden()
1566 raise HTTPForbidden()
1558
1567
1559 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1568 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1560 super_admin = h.HasPermissionAny('hg.admin')()
1569 super_admin = h.HasPermissionAny('hg.admin')()
1561 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1570 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1562 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1571 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1563 comment_repo_admin = is_repo_admin and is_repo_comment
1572 comment_repo_admin = is_repo_admin and is_repo_comment
1564
1573
1565 if super_admin or comment_owner or comment_repo_admin:
1574 if super_admin or comment_owner or comment_repo_admin:
1566 text = self.request.POST.get('text')
1575 text = self.request.POST.get('text')
1567 version = self.request.POST.get('version')
1576 version = self.request.POST.get('version')
1568 if text == comment.text:
1577 if text == comment.text:
1569 log.warning(
1578 log.warning(
1570 'Comment(PR): '
1579 'Comment(PR): '
1571 'Trying to create new version '
1580 'Trying to create new version '
1572 'with the same comment body {}'.format(
1581 'with the same comment body {}'.format(
1573 comment_id,
1582 comment_id,
1574 )
1583 )
1575 )
1584 )
1576 raise HTTPNotFound()
1585 raise HTTPNotFound()
1577
1586
1578 if version.isdigit():
1587 if version.isdigit():
1579 version = int(version)
1588 version = int(version)
1580 else:
1589 else:
1581 log.warning(
1590 log.warning(
1582 'Comment(PR): Wrong version type {} {} '
1591 'Comment(PR): Wrong version type {} {} '
1583 'for comment {}'.format(
1592 'for comment {}'.format(
1584 version,
1593 version,
1585 type(version),
1594 type(version),
1586 comment_id,
1595 comment_id,
1587 )
1596 )
1588 )
1597 )
1589 raise HTTPNotFound()
1598 raise HTTPNotFound()
1590
1599
1591 try:
1600 try:
1592 comment_history = CommentsModel().edit(
1601 comment_history = CommentsModel().edit(
1593 comment_id=comment_id,
1602 comment_id=comment_id,
1594 text=text,
1603 text=text,
1595 auth_user=self._rhodecode_user,
1604 auth_user=self._rhodecode_user,
1596 version=version,
1605 version=version,
1597 )
1606 )
1598 except CommentVersionMismatch:
1607 except CommentVersionMismatch:
1599 raise HTTPConflict()
1608 raise HTTPConflict()
1600
1609
1601 if not comment_history:
1610 if not comment_history:
1602 raise HTTPNotFound()
1611 raise HTTPNotFound()
1603
1612
1604 Session().commit()
1613 Session().commit()
1605 return {
1614 return {
1606 'comment_history_id': comment_history.comment_history_id,
1615 'comment_history_id': comment_history.comment_history_id,
1607 'comment_id': comment.comment_id,
1616 'comment_id': comment.comment_id,
1608 'comment_version': comment_history.version,
1617 'comment_version': comment_history.version,
1609 'comment_author_username': comment_history.author.username,
1618 'comment_author_username': comment_history.author.username,
1610 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1619 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1611 'comment_created_on': h.age_component(comment_history.created_on,
1620 'comment_created_on': h.age_component(comment_history.created_on,
1612 time_is_local=True),
1621 time_is_local=True),
1613 }
1622 }
1614 else:
1623 else:
1615 log.warning('No permissions for user %s to edit comment_id: %s',
1624 log.warning('No permissions for user %s to edit comment_id: %s',
1616 self._rhodecode_db_user, comment_id)
1625 self._rhodecode_db_user, comment_id)
1617 raise HTTPNotFound()
1626 raise HTTPNotFound()
@@ -1,1831 +1,1833 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 collections
21 import collections
22 import datetime
22 import datetime
23 import hashlib
23 import hashlib
24 import os
24 import os
25 import re
25 import re
26 import pprint
26 import pprint
27 import shutil
27 import shutil
28 import socket
28 import socket
29 import subprocess32
29 import subprocess32
30 import time
30 import time
31 import uuid
31 import uuid
32 import dateutil.tz
32 import dateutil.tz
33
33
34 import mock
34 import mock
35 import pyramid.testing
35 import pyramid.testing
36 import pytest
36 import pytest
37 import colander
37 import colander
38 import requests
38 import requests
39 import pyramid.paster
39 import pyramid.paster
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.lib.utils2 import AttributeDict
42 from rhodecode.lib.utils2 import AttributeDict
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 (
45 from rhodecode.model.db import (
46 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
46 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
47 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
47 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo_group import RepoGroupModel
51 from rhodecode.model.repo_group import RepoGroupModel
52 from rhodecode.model.user import UserModel
52 from rhodecode.model.user import UserModel
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54 from rhodecode.model.user_group import UserGroupModel
54 from rhodecode.model.user_group import UserGroupModel
55 from rhodecode.model.integration import IntegrationModel
55 from rhodecode.model.integration import IntegrationModel
56 from rhodecode.integrations import integration_type_registry
56 from rhodecode.integrations import integration_type_registry
57 from rhodecode.integrations.types.base import IntegrationTypeBase
57 from rhodecode.integrations.types.base import IntegrationTypeBase
58 from rhodecode.lib.utils import repo2db_mapper
58 from rhodecode.lib.utils import repo2db_mapper
59 from rhodecode.lib.vcs.backends import get_backend
59 from rhodecode.lib.vcs.backends import get_backend
60 from rhodecode.lib.vcs.nodes import FileNode
60 from rhodecode.lib.vcs.nodes import FileNode
61 from rhodecode.tests import (
61 from rhodecode.tests import (
62 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
62 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
63 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
63 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
64 TEST_USER_REGULAR_PASS)
64 TEST_USER_REGULAR_PASS)
65 from rhodecode.tests.utils import CustomTestApp, set_anonymous_access
65 from rhodecode.tests.utils import CustomTestApp, set_anonymous_access
66 from rhodecode.tests.fixture import Fixture
66 from rhodecode.tests.fixture import Fixture
67 from rhodecode.config import utils as config_utils
67 from rhodecode.config import utils as config_utils
68
68
69
69
70 def _split_comma(value):
70 def _split_comma(value):
71 return value.split(',')
71 return value.split(',')
72
72
73
73
74 def pytest_addoption(parser):
74 def pytest_addoption(parser):
75 parser.addoption(
75 parser.addoption(
76 '--keep-tmp-path', action='store_true',
76 '--keep-tmp-path', action='store_true',
77 help="Keep the test temporary directories")
77 help="Keep the test temporary directories")
78 parser.addoption(
78 parser.addoption(
79 '--backends', action='store', type=_split_comma,
79 '--backends', action='store', type=_split_comma,
80 default=['git', 'hg', 'svn'],
80 default=['git', 'hg', 'svn'],
81 help="Select which backends to test for backend specific tests.")
81 help="Select which backends to test for backend specific tests.")
82 parser.addoption(
82 parser.addoption(
83 '--dbs', action='store', type=_split_comma,
83 '--dbs', action='store', type=_split_comma,
84 default=['sqlite'],
84 default=['sqlite'],
85 help="Select which database to test for database specific tests. "
85 help="Select which database to test for database specific tests. "
86 "Possible options are sqlite,postgres,mysql")
86 "Possible options are sqlite,postgres,mysql")
87 parser.addoption(
87 parser.addoption(
88 '--appenlight', '--ae', action='store_true',
88 '--appenlight', '--ae', action='store_true',
89 help="Track statistics in appenlight.")
89 help="Track statistics in appenlight.")
90 parser.addoption(
90 parser.addoption(
91 '--appenlight-api-key', '--ae-key',
91 '--appenlight-api-key', '--ae-key',
92 help="API key for Appenlight.")
92 help="API key for Appenlight.")
93 parser.addoption(
93 parser.addoption(
94 '--appenlight-url', '--ae-url',
94 '--appenlight-url', '--ae-url',
95 default="https://ae.rhodecode.com",
95 default="https://ae.rhodecode.com",
96 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
96 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
97 parser.addoption(
97 parser.addoption(
98 '--sqlite-connection-string', action='store',
98 '--sqlite-connection-string', action='store',
99 default='', help="Connection string for the dbs tests with SQLite")
99 default='', help="Connection string for the dbs tests with SQLite")
100 parser.addoption(
100 parser.addoption(
101 '--postgres-connection-string', action='store',
101 '--postgres-connection-string', action='store',
102 default='', help="Connection string for the dbs tests with Postgres")
102 default='', help="Connection string for the dbs tests with Postgres")
103 parser.addoption(
103 parser.addoption(
104 '--mysql-connection-string', action='store',
104 '--mysql-connection-string', action='store',
105 default='', help="Connection string for the dbs tests with MySQL")
105 default='', help="Connection string for the dbs tests with MySQL")
106 parser.addoption(
106 parser.addoption(
107 '--repeat', type=int, default=100,
107 '--repeat', type=int, default=100,
108 help="Number of repetitions in performance tests.")
108 help="Number of repetitions in performance tests.")
109
109
110
110
111 def pytest_configure(config):
111 def pytest_configure(config):
112 from rhodecode.config import patches
112 from rhodecode.config import patches
113
113
114
114
115 def pytest_collection_modifyitems(session, config, items):
115 def pytest_collection_modifyitems(session, config, items):
116 # nottest marked, compare nose, used for transition from nose to pytest
116 # nottest marked, compare nose, used for transition from nose to pytest
117 remaining = [
117 remaining = [
118 i for i in items if getattr(i.obj, '__test__', True)]
118 i for i in items if getattr(i.obj, '__test__', True)]
119 items[:] = remaining
119 items[:] = remaining
120
120
121 # NOTE(marcink): custom test ordering, db tests and vcstests are slowes and should
121 # NOTE(marcink): custom test ordering, db tests and vcstests are slowes and should
122 # be executed at the end for faster test feedback
122 # be executed at the end for faster test feedback
123 def sorter(item):
123 def sorter(item):
124 pos = 0
124 pos = 0
125 key = item._nodeid
125 key = item._nodeid
126 if key.startswith('rhodecode/tests/database'):
126 if key.startswith('rhodecode/tests/database'):
127 pos = 1
127 pos = 1
128 elif key.startswith('rhodecode/tests/vcs_operations'):
128 elif key.startswith('rhodecode/tests/vcs_operations'):
129 pos = 2
129 pos = 2
130
130
131 return pos
131 return pos
132
132
133 items.sort(key=sorter)
133 items.sort(key=sorter)
134
134
135
135
136 def pytest_generate_tests(metafunc):
136 def pytest_generate_tests(metafunc):
137
137
138 # Support test generation based on --backend parameter
138 # Support test generation based on --backend parameter
139 if 'backend_alias' in metafunc.fixturenames:
139 if 'backend_alias' in metafunc.fixturenames:
140 backends = get_backends_from_metafunc(metafunc)
140 backends = get_backends_from_metafunc(metafunc)
141 scope = None
141 scope = None
142 if not backends:
142 if not backends:
143 pytest.skip("Not enabled for any of selected backends")
143 pytest.skip("Not enabled for any of selected backends")
144
144
145 metafunc.parametrize('backend_alias', backends, scope=scope)
145 metafunc.parametrize('backend_alias', backends, scope=scope)
146
146
147 backend_mark = metafunc.definition.get_closest_marker('backends')
147 backend_mark = metafunc.definition.get_closest_marker('backends')
148 if backend_mark:
148 if backend_mark:
149 backends = get_backends_from_metafunc(metafunc)
149 backends = get_backends_from_metafunc(metafunc)
150 if not backends:
150 if not backends:
151 pytest.skip("Not enabled for any of selected backends")
151 pytest.skip("Not enabled for any of selected backends")
152
152
153
153
154 def get_backends_from_metafunc(metafunc):
154 def get_backends_from_metafunc(metafunc):
155 requested_backends = set(metafunc.config.getoption('--backends'))
155 requested_backends = set(metafunc.config.getoption('--backends'))
156 backend_mark = metafunc.definition.get_closest_marker('backends')
156 backend_mark = metafunc.definition.get_closest_marker('backends')
157 if backend_mark:
157 if backend_mark:
158 # Supported backends by this test function, created from
158 # Supported backends by this test function, created from
159 # pytest.mark.backends
159 # pytest.mark.backends
160 backends = backend_mark.args
160 backends = backend_mark.args
161 elif hasattr(metafunc.cls, 'backend_alias'):
161 elif hasattr(metafunc.cls, 'backend_alias'):
162 # Support class attribute "backend_alias", this is mainly
162 # Support class attribute "backend_alias", this is mainly
163 # for legacy reasons for tests not yet using pytest.mark.backends
163 # for legacy reasons for tests not yet using pytest.mark.backends
164 backends = [metafunc.cls.backend_alias]
164 backends = [metafunc.cls.backend_alias]
165 else:
165 else:
166 backends = metafunc.config.getoption('--backends')
166 backends = metafunc.config.getoption('--backends')
167 return requested_backends.intersection(backends)
167 return requested_backends.intersection(backends)
168
168
169
169
170 @pytest.fixture(scope='session', autouse=True)
170 @pytest.fixture(scope='session', autouse=True)
171 def activate_example_rcextensions(request):
171 def activate_example_rcextensions(request):
172 """
172 """
173 Patch in an example rcextensions module which verifies passed in kwargs.
173 Patch in an example rcextensions module which verifies passed in kwargs.
174 """
174 """
175 from rhodecode.config import rcextensions
175 from rhodecode.config import rcextensions
176
176
177 old_extensions = rhodecode.EXTENSIONS
177 old_extensions = rhodecode.EXTENSIONS
178 rhodecode.EXTENSIONS = rcextensions
178 rhodecode.EXTENSIONS = rcextensions
179 rhodecode.EXTENSIONS.calls = collections.defaultdict(list)
179 rhodecode.EXTENSIONS.calls = collections.defaultdict(list)
180
180
181 @request.addfinalizer
181 @request.addfinalizer
182 def cleanup():
182 def cleanup():
183 rhodecode.EXTENSIONS = old_extensions
183 rhodecode.EXTENSIONS = old_extensions
184
184
185
185
186 @pytest.fixture()
186 @pytest.fixture()
187 def capture_rcextensions():
187 def capture_rcextensions():
188 """
188 """
189 Returns the recorded calls to entry points in rcextensions.
189 Returns the recorded calls to entry points in rcextensions.
190 """
190 """
191 calls = rhodecode.EXTENSIONS.calls
191 calls = rhodecode.EXTENSIONS.calls
192 calls.clear()
192 calls.clear()
193 # Note: At this moment, it is still the empty dict, but that will
193 # Note: At this moment, it is still the empty dict, but that will
194 # be filled during the test run and since it is a reference this
194 # be filled during the test run and since it is a reference this
195 # is enough to make it work.
195 # is enough to make it work.
196 return calls
196 return calls
197
197
198
198
199 @pytest.fixture(scope='session')
199 @pytest.fixture(scope='session')
200 def http_environ_session():
200 def http_environ_session():
201 """
201 """
202 Allow to use "http_environ" in session scope.
202 Allow to use "http_environ" in session scope.
203 """
203 """
204 return plain_http_environ()
204 return plain_http_environ()
205
205
206
206
207 def plain_http_host_stub():
207 def plain_http_host_stub():
208 """
208 """
209 Value of HTTP_HOST in the test run.
209 Value of HTTP_HOST in the test run.
210 """
210 """
211 return 'example.com:80'
211 return 'example.com:80'
212
212
213
213
214 @pytest.fixture()
214 @pytest.fixture()
215 def http_host_stub():
215 def http_host_stub():
216 """
216 """
217 Value of HTTP_HOST in the test run.
217 Value of HTTP_HOST in the test run.
218 """
218 """
219 return plain_http_host_stub()
219 return plain_http_host_stub()
220
220
221
221
222 def plain_http_host_only_stub():
222 def plain_http_host_only_stub():
223 """
223 """
224 Value of HTTP_HOST in the test run.
224 Value of HTTP_HOST in the test run.
225 """
225 """
226 return plain_http_host_stub().split(':')[0]
226 return plain_http_host_stub().split(':')[0]
227
227
228
228
229 @pytest.fixture()
229 @pytest.fixture()
230 def http_host_only_stub():
230 def http_host_only_stub():
231 """
231 """
232 Value of HTTP_HOST in the test run.
232 Value of HTTP_HOST in the test run.
233 """
233 """
234 return plain_http_host_only_stub()
234 return plain_http_host_only_stub()
235
235
236
236
237 def plain_http_environ():
237 def plain_http_environ():
238 """
238 """
239 HTTP extra environ keys.
239 HTTP extra environ keys.
240
240
241 User by the test application and as well for setting up the pylons
241 User by the test application and as well for setting up the pylons
242 environment. In the case of the fixture "app" it should be possible
242 environment. In the case of the fixture "app" it should be possible
243 to override this for a specific test case.
243 to override this for a specific test case.
244 """
244 """
245 return {
245 return {
246 'SERVER_NAME': plain_http_host_only_stub(),
246 'SERVER_NAME': plain_http_host_only_stub(),
247 'SERVER_PORT': plain_http_host_stub().split(':')[1],
247 'SERVER_PORT': plain_http_host_stub().split(':')[1],
248 'HTTP_HOST': plain_http_host_stub(),
248 'HTTP_HOST': plain_http_host_stub(),
249 'HTTP_USER_AGENT': 'rc-test-agent',
249 'HTTP_USER_AGENT': 'rc-test-agent',
250 'REQUEST_METHOD': 'GET'
250 'REQUEST_METHOD': 'GET'
251 }
251 }
252
252
253
253
254 @pytest.fixture()
254 @pytest.fixture()
255 def http_environ():
255 def http_environ():
256 """
256 """
257 HTTP extra environ keys.
257 HTTP extra environ keys.
258
258
259 User by the test application and as well for setting up the pylons
259 User by the test application and as well for setting up the pylons
260 environment. In the case of the fixture "app" it should be possible
260 environment. In the case of the fixture "app" it should be possible
261 to override this for a specific test case.
261 to override this for a specific test case.
262 """
262 """
263 return plain_http_environ()
263 return plain_http_environ()
264
264
265
265
266 @pytest.fixture(scope='session')
266 @pytest.fixture(scope='session')
267 def baseapp(ini_config, vcsserver, http_environ_session):
267 def baseapp(ini_config, vcsserver, http_environ_session):
268 from rhodecode.lib.pyramid_utils import get_app_config
268 from rhodecode.lib.pyramid_utils import get_app_config
269 from rhodecode.config.middleware import make_pyramid_app
269 from rhodecode.config.middleware import make_pyramid_app
270
270
271 print("Using the RhodeCode configuration:{}".format(ini_config))
271 print("Using the RhodeCode configuration:{}".format(ini_config))
272 pyramid.paster.setup_logging(ini_config)
272 pyramid.paster.setup_logging(ini_config)
273
273
274 settings = get_app_config(ini_config)
274 settings = get_app_config(ini_config)
275 app = make_pyramid_app({'__file__': ini_config}, **settings)
275 app = make_pyramid_app({'__file__': ini_config}, **settings)
276
276
277 return app
277 return app
278
278
279
279
280 @pytest.fixture(scope='function')
280 @pytest.fixture(scope='function')
281 def app(request, config_stub, baseapp, http_environ):
281 def app(request, config_stub, baseapp, http_environ):
282 app = CustomTestApp(
282 app = CustomTestApp(
283 baseapp,
283 baseapp,
284 extra_environ=http_environ)
284 extra_environ=http_environ)
285 if request.cls:
285 if request.cls:
286 request.cls.app = app
286 request.cls.app = app
287 return app
287 return app
288
288
289
289
290 @pytest.fixture(scope='session')
290 @pytest.fixture(scope='session')
291 def app_settings(baseapp, ini_config):
291 def app_settings(baseapp, ini_config):
292 """
292 """
293 Settings dictionary used to create the app.
293 Settings dictionary used to create the app.
294
294
295 Parses the ini file and passes the result through the sanitize and apply
295 Parses the ini file and passes the result through the sanitize and apply
296 defaults mechanism in `rhodecode.config.middleware`.
296 defaults mechanism in `rhodecode.config.middleware`.
297 """
297 """
298 return baseapp.config.get_settings()
298 return baseapp.config.get_settings()
299
299
300
300
301 @pytest.fixture(scope='session')
301 @pytest.fixture(scope='session')
302 def db_connection(ini_settings):
302 def db_connection(ini_settings):
303 # Initialize the database connection.
303 # Initialize the database connection.
304 config_utils.initialize_database(ini_settings)
304 config_utils.initialize_database(ini_settings)
305
305
306
306
307 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
307 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
308
308
309
309
310 def _autologin_user(app, *args):
310 def _autologin_user(app, *args):
311 session = login_user_session(app, *args)
311 session = login_user_session(app, *args)
312 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
312 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
313 return LoginData(csrf_token, session['rhodecode_user'])
313 return LoginData(csrf_token, session['rhodecode_user'])
314
314
315
315
316 @pytest.fixture()
316 @pytest.fixture()
317 def autologin_user(app):
317 def autologin_user(app):
318 """
318 """
319 Utility fixture which makes sure that the admin user is logged in
319 Utility fixture which makes sure that the admin user is logged in
320 """
320 """
321 return _autologin_user(app)
321 return _autologin_user(app)
322
322
323
323
324 @pytest.fixture()
324 @pytest.fixture()
325 def autologin_regular_user(app):
325 def autologin_regular_user(app):
326 """
326 """
327 Utility fixture which makes sure that the regular user is logged in
327 Utility fixture which makes sure that the regular user is logged in
328 """
328 """
329 return _autologin_user(
329 return _autologin_user(
330 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
330 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
331
331
332
332
333 @pytest.fixture(scope='function')
333 @pytest.fixture(scope='function')
334 def csrf_token(request, autologin_user):
334 def csrf_token(request, autologin_user):
335 return autologin_user.csrf_token
335 return autologin_user.csrf_token
336
336
337
337
338 @pytest.fixture(scope='function')
338 @pytest.fixture(scope='function')
339 def xhr_header(request):
339 def xhr_header(request):
340 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
340 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
341
341
342
342
343 @pytest.fixture()
343 @pytest.fixture()
344 def real_crypto_backend(monkeypatch):
344 def real_crypto_backend(monkeypatch):
345 """
345 """
346 Switch the production crypto backend on for this test.
346 Switch the production crypto backend on for this test.
347
347
348 During the test run the crypto backend is replaced with a faster
348 During the test run the crypto backend is replaced with a faster
349 implementation based on the MD5 algorithm.
349 implementation based on the MD5 algorithm.
350 """
350 """
351 monkeypatch.setattr(rhodecode, 'is_test', False)
351 monkeypatch.setattr(rhodecode, 'is_test', False)
352
352
353
353
354 @pytest.fixture(scope='class')
354 @pytest.fixture(scope='class')
355 def index_location(request, baseapp):
355 def index_location(request, baseapp):
356 index_location = baseapp.config.get_settings()['search.location']
356 index_location = baseapp.config.get_settings()['search.location']
357 if request.cls:
357 if request.cls:
358 request.cls.index_location = index_location
358 request.cls.index_location = index_location
359 return index_location
359 return index_location
360
360
361
361
362 @pytest.fixture(scope='session', autouse=True)
362 @pytest.fixture(scope='session', autouse=True)
363 def tests_tmp_path(request):
363 def tests_tmp_path(request):
364 """
364 """
365 Create temporary directory to be used during the test session.
365 Create temporary directory to be used during the test session.
366 """
366 """
367 if not os.path.exists(TESTS_TMP_PATH):
367 if not os.path.exists(TESTS_TMP_PATH):
368 os.makedirs(TESTS_TMP_PATH)
368 os.makedirs(TESTS_TMP_PATH)
369
369
370 if not request.config.getoption('--keep-tmp-path'):
370 if not request.config.getoption('--keep-tmp-path'):
371 @request.addfinalizer
371 @request.addfinalizer
372 def remove_tmp_path():
372 def remove_tmp_path():
373 shutil.rmtree(TESTS_TMP_PATH)
373 shutil.rmtree(TESTS_TMP_PATH)
374
374
375 return TESTS_TMP_PATH
375 return TESTS_TMP_PATH
376
376
377
377
378 @pytest.fixture()
378 @pytest.fixture()
379 def test_repo_group(request):
379 def test_repo_group(request):
380 """
380 """
381 Create a temporary repository group, and destroy it after
381 Create a temporary repository group, and destroy it after
382 usage automatically
382 usage automatically
383 """
383 """
384 fixture = Fixture()
384 fixture = Fixture()
385 repogroupid = 'test_repo_group_%s' % str(time.time()).replace('.', '')
385 repogroupid = 'test_repo_group_%s' % str(time.time()).replace('.', '')
386 repo_group = fixture.create_repo_group(repogroupid)
386 repo_group = fixture.create_repo_group(repogroupid)
387
387
388 def _cleanup():
388 def _cleanup():
389 fixture.destroy_repo_group(repogroupid)
389 fixture.destroy_repo_group(repogroupid)
390
390
391 request.addfinalizer(_cleanup)
391 request.addfinalizer(_cleanup)
392 return repo_group
392 return repo_group
393
393
394
394
395 @pytest.fixture()
395 @pytest.fixture()
396 def test_user_group(request):
396 def test_user_group(request):
397 """
397 """
398 Create a temporary user group, and destroy it after
398 Create a temporary user group, and destroy it after
399 usage automatically
399 usage automatically
400 """
400 """
401 fixture = Fixture()
401 fixture = Fixture()
402 usergroupid = 'test_user_group_%s' % str(time.time()).replace('.', '')
402 usergroupid = 'test_user_group_%s' % str(time.time()).replace('.', '')
403 user_group = fixture.create_user_group(usergroupid)
403 user_group = fixture.create_user_group(usergroupid)
404
404
405 def _cleanup():
405 def _cleanup():
406 fixture.destroy_user_group(user_group)
406 fixture.destroy_user_group(user_group)
407
407
408 request.addfinalizer(_cleanup)
408 request.addfinalizer(_cleanup)
409 return user_group
409 return user_group
410
410
411
411
412 @pytest.fixture(scope='session')
412 @pytest.fixture(scope='session')
413 def test_repo(request):
413 def test_repo(request):
414 container = TestRepoContainer()
414 container = TestRepoContainer()
415 request.addfinalizer(container._cleanup)
415 request.addfinalizer(container._cleanup)
416 return container
416 return container
417
417
418
418
419 class TestRepoContainer(object):
419 class TestRepoContainer(object):
420 """
420 """
421 Container for test repositories which are used read only.
421 Container for test repositories which are used read only.
422
422
423 Repositories will be created on demand and re-used during the lifetime
423 Repositories will be created on demand and re-used during the lifetime
424 of this object.
424 of this object.
425
425
426 Usage to get the svn test repository "minimal"::
426 Usage to get the svn test repository "minimal"::
427
427
428 test_repo = TestContainer()
428 test_repo = TestContainer()
429 repo = test_repo('minimal', 'svn')
429 repo = test_repo('minimal', 'svn')
430
430
431 """
431 """
432
432
433 dump_extractors = {
433 dump_extractors = {
434 'git': utils.extract_git_repo_from_dump,
434 'git': utils.extract_git_repo_from_dump,
435 'hg': utils.extract_hg_repo_from_dump,
435 'hg': utils.extract_hg_repo_from_dump,
436 'svn': utils.extract_svn_repo_from_dump,
436 'svn': utils.extract_svn_repo_from_dump,
437 }
437 }
438
438
439 def __init__(self):
439 def __init__(self):
440 self._cleanup_repos = []
440 self._cleanup_repos = []
441 self._fixture = Fixture()
441 self._fixture = Fixture()
442 self._repos = {}
442 self._repos = {}
443
443
444 def __call__(self, dump_name, backend_alias, config=None):
444 def __call__(self, dump_name, backend_alias, config=None):
445 key = (dump_name, backend_alias)
445 key = (dump_name, backend_alias)
446 if key not in self._repos:
446 if key not in self._repos:
447 repo = self._create_repo(dump_name, backend_alias, config)
447 repo = self._create_repo(dump_name, backend_alias, config)
448 self._repos[key] = repo.repo_id
448 self._repos[key] = repo.repo_id
449 return Repository.get(self._repos[key])
449 return Repository.get(self._repos[key])
450
450
451 def _create_repo(self, dump_name, backend_alias, config):
451 def _create_repo(self, dump_name, backend_alias, config):
452 repo_name = '%s-%s' % (backend_alias, dump_name)
452 repo_name = '%s-%s' % (backend_alias, dump_name)
453 backend = get_backend(backend_alias)
453 backend = get_backend(backend_alias)
454 dump_extractor = self.dump_extractors[backend_alias]
454 dump_extractor = self.dump_extractors[backend_alias]
455 repo_path = dump_extractor(dump_name, repo_name)
455 repo_path = dump_extractor(dump_name, repo_name)
456
456
457 vcs_repo = backend(repo_path, config=config)
457 vcs_repo = backend(repo_path, config=config)
458 repo2db_mapper({repo_name: vcs_repo})
458 repo2db_mapper({repo_name: vcs_repo})
459
459
460 repo = RepoModel().get_by_repo_name(repo_name)
460 repo = RepoModel().get_by_repo_name(repo_name)
461 self._cleanup_repos.append(repo_name)
461 self._cleanup_repos.append(repo_name)
462 return repo
462 return repo
463
463
464 def _cleanup(self):
464 def _cleanup(self):
465 for repo_name in reversed(self._cleanup_repos):
465 for repo_name in reversed(self._cleanup_repos):
466 self._fixture.destroy_repo(repo_name)
466 self._fixture.destroy_repo(repo_name)
467
467
468
468
469 def backend_base(request, backend_alias, baseapp, test_repo):
469 def backend_base(request, backend_alias, baseapp, test_repo):
470 if backend_alias not in request.config.getoption('--backends'):
470 if backend_alias not in request.config.getoption('--backends'):
471 pytest.skip("Backend %s not selected." % (backend_alias, ))
471 pytest.skip("Backend %s not selected." % (backend_alias, ))
472
472
473 utils.check_xfail_backends(request.node, backend_alias)
473 utils.check_xfail_backends(request.node, backend_alias)
474 utils.check_skip_backends(request.node, backend_alias)
474 utils.check_skip_backends(request.node, backend_alias)
475
475
476 repo_name = 'vcs_test_%s' % (backend_alias, )
476 repo_name = 'vcs_test_%s' % (backend_alias, )
477 backend = Backend(
477 backend = Backend(
478 alias=backend_alias,
478 alias=backend_alias,
479 repo_name=repo_name,
479 repo_name=repo_name,
480 test_name=request.node.name,
480 test_name=request.node.name,
481 test_repo_container=test_repo)
481 test_repo_container=test_repo)
482 request.addfinalizer(backend.cleanup)
482 request.addfinalizer(backend.cleanup)
483 return backend
483 return backend
484
484
485
485
486 @pytest.fixture()
486 @pytest.fixture()
487 def backend(request, backend_alias, baseapp, test_repo):
487 def backend(request, backend_alias, baseapp, test_repo):
488 """
488 """
489 Parametrized fixture which represents a single backend implementation.
489 Parametrized fixture which represents a single backend implementation.
490
490
491 It respects the option `--backends` to focus the test run on specific
491 It respects the option `--backends` to focus the test run on specific
492 backend implementations.
492 backend implementations.
493
493
494 It also supports `pytest.mark.xfail_backends` to mark tests as failing
494 It also supports `pytest.mark.xfail_backends` to mark tests as failing
495 for specific backends. This is intended as a utility for incremental
495 for specific backends. This is intended as a utility for incremental
496 development of a new backend implementation.
496 development of a new backend implementation.
497 """
497 """
498 return backend_base(request, backend_alias, baseapp, test_repo)
498 return backend_base(request, backend_alias, baseapp, test_repo)
499
499
500
500
501 @pytest.fixture()
501 @pytest.fixture()
502 def backend_git(request, baseapp, test_repo):
502 def backend_git(request, baseapp, test_repo):
503 return backend_base(request, 'git', baseapp, test_repo)
503 return backend_base(request, 'git', baseapp, test_repo)
504
504
505
505
506 @pytest.fixture()
506 @pytest.fixture()
507 def backend_hg(request, baseapp, test_repo):
507 def backend_hg(request, baseapp, test_repo):
508 return backend_base(request, 'hg', baseapp, test_repo)
508 return backend_base(request, 'hg', baseapp, test_repo)
509
509
510
510
511 @pytest.fixture()
511 @pytest.fixture()
512 def backend_svn(request, baseapp, test_repo):
512 def backend_svn(request, baseapp, test_repo):
513 return backend_base(request, 'svn', baseapp, test_repo)
513 return backend_base(request, 'svn', baseapp, test_repo)
514
514
515
515
516 @pytest.fixture()
516 @pytest.fixture()
517 def backend_random(backend_git):
517 def backend_random(backend_git):
518 """
518 """
519 Use this to express that your tests need "a backend.
519 Use this to express that your tests need "a backend.
520
520
521 A few of our tests need a backend, so that we can run the code. This
521 A few of our tests need a backend, so that we can run the code. This
522 fixture is intended to be used for such cases. It will pick one of the
522 fixture is intended to be used for such cases. It will pick one of the
523 backends and run the tests.
523 backends and run the tests.
524
524
525 The fixture `backend` would run the test multiple times for each
525 The fixture `backend` would run the test multiple times for each
526 available backend which is a pure waste of time if the test is
526 available backend which is a pure waste of time if the test is
527 independent of the backend type.
527 independent of the backend type.
528 """
528 """
529 # TODO: johbo: Change this to pick a random backend
529 # TODO: johbo: Change this to pick a random backend
530 return backend_git
530 return backend_git
531
531
532
532
533 @pytest.fixture()
533 @pytest.fixture()
534 def backend_stub(backend_git):
534 def backend_stub(backend_git):
535 """
535 """
536 Use this to express that your tests need a backend stub
536 Use this to express that your tests need a backend stub
537
537
538 TODO: mikhail: Implement a real stub logic instead of returning
538 TODO: mikhail: Implement a real stub logic instead of returning
539 a git backend
539 a git backend
540 """
540 """
541 return backend_git
541 return backend_git
542
542
543
543
544 @pytest.fixture()
544 @pytest.fixture()
545 def repo_stub(backend_stub):
545 def repo_stub(backend_stub):
546 """
546 """
547 Use this to express that your tests need a repository stub
547 Use this to express that your tests need a repository stub
548 """
548 """
549 return backend_stub.create_repo()
549 return backend_stub.create_repo()
550
550
551
551
552 class Backend(object):
552 class Backend(object):
553 """
553 """
554 Represents the test configuration for one supported backend
554 Represents the test configuration for one supported backend
555
555
556 Provides easy access to different test repositories based on
556 Provides easy access to different test repositories based on
557 `__getitem__`. Such repositories will only be created once per test
557 `__getitem__`. Such repositories will only be created once per test
558 session.
558 session.
559 """
559 """
560
560
561 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
561 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
562 _master_repo = None
562 _master_repo = None
563 _master_repo_path = ''
563 _commit_ids = {}
564 _commit_ids = {}
564
565
565 def __init__(self, alias, repo_name, test_name, test_repo_container):
566 def __init__(self, alias, repo_name, test_name, test_repo_container):
566 self.alias = alias
567 self.alias = alias
567 self.repo_name = repo_name
568 self.repo_name = repo_name
568 self._cleanup_repos = []
569 self._cleanup_repos = []
569 self._test_name = test_name
570 self._test_name = test_name
570 self._test_repo_container = test_repo_container
571 self._test_repo_container = test_repo_container
571 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
572 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
572 # Fixture will survive in the end.
573 # Fixture will survive in the end.
573 self._fixture = Fixture()
574 self._fixture = Fixture()
574
575
575 def __getitem__(self, key):
576 def __getitem__(self, key):
576 return self._test_repo_container(key, self.alias)
577 return self._test_repo_container(key, self.alias)
577
578
578 def create_test_repo(self, key, config=None):
579 def create_test_repo(self, key, config=None):
579 return self._test_repo_container(key, self.alias, config)
580 return self._test_repo_container(key, self.alias, config)
580
581
581 @property
582 @property
582 def repo(self):
583 def repo(self):
583 """
584 """
584 Returns the "current" repository. This is the vcs_test repo or the
585 Returns the "current" repository. This is the vcs_test repo or the
585 last repo which has been created with `create_repo`.
586 last repo which has been created with `create_repo`.
586 """
587 """
587 from rhodecode.model.db import Repository
588 from rhodecode.model.db import Repository
588 return Repository.get_by_repo_name(self.repo_name)
589 return Repository.get_by_repo_name(self.repo_name)
589
590
590 @property
591 @property
591 def default_branch_name(self):
592 def default_branch_name(self):
592 VcsRepository = get_backend(self.alias)
593 VcsRepository = get_backend(self.alias)
593 return VcsRepository.DEFAULT_BRANCH_NAME
594 return VcsRepository.DEFAULT_BRANCH_NAME
594
595
595 @property
596 @property
596 def default_head_id(self):
597 def default_head_id(self):
597 """
598 """
598 Returns the default head id of the underlying backend.
599 Returns the default head id of the underlying backend.
599
600
600 This will be the default branch name in case the backend does have a
601 This will be the default branch name in case the backend does have a
601 default branch. In the other cases it will point to a valid head
602 default branch. In the other cases it will point to a valid head
602 which can serve as the base to create a new commit on top of it.
603 which can serve as the base to create a new commit on top of it.
603 """
604 """
604 vcsrepo = self.repo.scm_instance()
605 vcsrepo = self.repo.scm_instance()
605 head_id = (
606 head_id = (
606 vcsrepo.DEFAULT_BRANCH_NAME or
607 vcsrepo.DEFAULT_BRANCH_NAME or
607 vcsrepo.commit_ids[-1])
608 vcsrepo.commit_ids[-1])
608 return head_id
609 return head_id
609
610
610 @property
611 @property
611 def commit_ids(self):
612 def commit_ids(self):
612 """
613 """
613 Returns the list of commits for the last created repository
614 Returns the list of commits for the last created repository
614 """
615 """
615 return self._commit_ids
616 return self._commit_ids
616
617
617 def create_master_repo(self, commits):
618 def create_master_repo(self, commits):
618 """
619 """
619 Create a repository and remember it as a template.
620 Create a repository and remember it as a template.
620
621
621 This allows to easily create derived repositories to construct
622 This allows to easily create derived repositories to construct
622 more complex scenarios for diff, compare and pull requests.
623 more complex scenarios for diff, compare and pull requests.
623
624
624 Returns a commit map which maps from commit message to raw_id.
625 Returns a commit map which maps from commit message to raw_id.
625 """
626 """
626 self._master_repo = self.create_repo(commits=commits)
627 self._master_repo = self.create_repo(commits=commits)
628 self._master_repo_path = self._master_repo.repo_full_path
629
627 return self._commit_ids
630 return self._commit_ids
628
631
629 def create_repo(
632 def create_repo(
630 self, commits=None, number_of_commits=0, heads=None,
633 self, commits=None, number_of_commits=0, heads=None,
631 name_suffix=u'', bare=False, **kwargs):
634 name_suffix=u'', bare=False, **kwargs):
632 """
635 """
633 Create a repository and record it for later cleanup.
636 Create a repository and record it for later cleanup.
634
637
635 :param commits: Optional. A sequence of dict instances.
638 :param commits: Optional. A sequence of dict instances.
636 Will add a commit per entry to the new repository.
639 Will add a commit per entry to the new repository.
637 :param number_of_commits: Optional. If set to a number, this number of
640 :param number_of_commits: Optional. If set to a number, this number of
638 commits will be added to the new repository.
641 commits will be added to the new repository.
639 :param heads: Optional. Can be set to a sequence of of commit
642 :param heads: Optional. Can be set to a sequence of of commit
640 names which shall be pulled in from the master repository.
643 names which shall be pulled in from the master repository.
641 :param name_suffix: adds special suffix to generated repo name
644 :param name_suffix: adds special suffix to generated repo name
642 :param bare: set a repo as bare (no checkout)
645 :param bare: set a repo as bare (no checkout)
643 """
646 """
644 self.repo_name = self._next_repo_name() + name_suffix
647 self.repo_name = self._next_repo_name() + name_suffix
645 repo = self._fixture.create_repo(
648 repo = self._fixture.create_repo(
646 self.repo_name, repo_type=self.alias, bare=bare, **kwargs)
649 self.repo_name, repo_type=self.alias, bare=bare, **kwargs)
647 self._cleanup_repos.append(repo.repo_name)
650 self._cleanup_repos.append(repo.repo_name)
648
651
649 commits = commits or [
652 commits = commits or [
650 {'message': 'Commit %s of %s' % (x, self.repo_name)}
653 {'message': 'Commit %s of %s' % (x, self.repo_name)}
651 for x in range(number_of_commits)]
654 for x in range(number_of_commits)]
652 vcs_repo = repo.scm_instance()
655 vcs_repo = repo.scm_instance()
653 vcs_repo.count()
656 vcs_repo.count()
654 self._add_commits_to_repo(vcs_repo, commits)
657 self._add_commits_to_repo(vcs_repo, commits)
655 if heads:
658 if heads:
656 self.pull_heads(repo, heads)
659 self.pull_heads(repo, heads)
657
660
658 return repo
661 return repo
659
662
660 def pull_heads(self, repo, heads):
663 def pull_heads(self, repo, heads):
661 """
664 """
662 Make sure that repo contains all commits mentioned in `heads`
665 Make sure that repo contains all commits mentioned in `heads`
663 """
666 """
664 vcsmaster = self._master_repo.scm_instance()
665 vcsrepo = repo.scm_instance()
667 vcsrepo = repo.scm_instance()
666 vcsrepo.config.clear_section('hooks')
668 vcsrepo.config.clear_section('hooks')
667 commit_ids = [self._commit_ids[h] for h in heads]
669 commit_ids = [self._commit_ids[h] for h in heads]
668 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
670 vcsrepo.pull(self._master_repo_path, commit_ids=commit_ids)
669
671
670 def create_fork(self):
672 def create_fork(self):
671 repo_to_fork = self.repo_name
673 repo_to_fork = self.repo_name
672 self.repo_name = self._next_repo_name()
674 self.repo_name = self._next_repo_name()
673 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
675 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
674 self._cleanup_repos.append(self.repo_name)
676 self._cleanup_repos.append(self.repo_name)
675 return repo
677 return repo
676
678
677 def new_repo_name(self, suffix=u''):
679 def new_repo_name(self, suffix=u''):
678 self.repo_name = self._next_repo_name() + suffix
680 self.repo_name = self._next_repo_name() + suffix
679 self._cleanup_repos.append(self.repo_name)
681 self._cleanup_repos.append(self.repo_name)
680 return self.repo_name
682 return self.repo_name
681
683
682 def _next_repo_name(self):
684 def _next_repo_name(self):
683 return u"%s_%s" % (
685 return u"%s_%s" % (
684 self.invalid_repo_name.sub(u'_', self._test_name), len(self._cleanup_repos))
686 self.invalid_repo_name.sub(u'_', self._test_name), len(self._cleanup_repos))
685
687
686 def ensure_file(self, filename, content='Test content\n'):
688 def ensure_file(self, filename, content='Test content\n'):
687 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
689 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
688 commits = [
690 commits = [
689 {'added': [
691 {'added': [
690 FileNode(filename, content=content),
692 FileNode(filename, content=content),
691 ]},
693 ]},
692 ]
694 ]
693 self._add_commits_to_repo(self.repo.scm_instance(), commits)
695 self._add_commits_to_repo(self.repo.scm_instance(), commits)
694
696
695 def enable_downloads(self):
697 def enable_downloads(self):
696 repo = self.repo
698 repo = self.repo
697 repo.enable_downloads = True
699 repo.enable_downloads = True
698 Session().add(repo)
700 Session().add(repo)
699 Session().commit()
701 Session().commit()
700
702
701 def cleanup(self):
703 def cleanup(self):
702 for repo_name in reversed(self._cleanup_repos):
704 for repo_name in reversed(self._cleanup_repos):
703 self._fixture.destroy_repo(repo_name)
705 self._fixture.destroy_repo(repo_name)
704
706
705 def _add_commits_to_repo(self, repo, commits):
707 def _add_commits_to_repo(self, repo, commits):
706 commit_ids = _add_commits_to_repo(repo, commits)
708 commit_ids = _add_commits_to_repo(repo, commits)
707 if not commit_ids:
709 if not commit_ids:
708 return
710 return
709 self._commit_ids = commit_ids
711 self._commit_ids = commit_ids
710
712
711 # Creating refs for Git to allow fetching them from remote repository
713 # Creating refs for Git to allow fetching them from remote repository
712 if self.alias == 'git':
714 if self.alias == 'git':
713 refs = {}
715 refs = {}
714 for message in self._commit_ids:
716 for message in self._commit_ids:
715 # TODO: mikhail: do more special chars replacements
717 # TODO: mikhail: do more special chars replacements
716 ref_name = 'refs/test-refs/{}'.format(
718 ref_name = 'refs/test-refs/{}'.format(
717 message.replace(' ', ''))
719 message.replace(' ', ''))
718 refs[ref_name] = self._commit_ids[message]
720 refs[ref_name] = self._commit_ids[message]
719 self._create_refs(repo, refs)
721 self._create_refs(repo, refs)
720
722
721 def _create_refs(self, repo, refs):
723 def _create_refs(self, repo, refs):
722 for ref_name in refs:
724 for ref_name in refs:
723 repo.set_refs(ref_name, refs[ref_name])
725 repo.set_refs(ref_name, refs[ref_name])
724
726
725
727
726 def vcsbackend_base(request, backend_alias, tests_tmp_path, baseapp, test_repo):
728 def vcsbackend_base(request, backend_alias, tests_tmp_path, baseapp, test_repo):
727 if backend_alias not in request.config.getoption('--backends'):
729 if backend_alias not in request.config.getoption('--backends'):
728 pytest.skip("Backend %s not selected." % (backend_alias, ))
730 pytest.skip("Backend %s not selected." % (backend_alias, ))
729
731
730 utils.check_xfail_backends(request.node, backend_alias)
732 utils.check_xfail_backends(request.node, backend_alias)
731 utils.check_skip_backends(request.node, backend_alias)
733 utils.check_skip_backends(request.node, backend_alias)
732
734
733 repo_name = 'vcs_test_%s' % (backend_alias, )
735 repo_name = 'vcs_test_%s' % (backend_alias, )
734 repo_path = os.path.join(tests_tmp_path, repo_name)
736 repo_path = os.path.join(tests_tmp_path, repo_name)
735 backend = VcsBackend(
737 backend = VcsBackend(
736 alias=backend_alias,
738 alias=backend_alias,
737 repo_path=repo_path,
739 repo_path=repo_path,
738 test_name=request.node.name,
740 test_name=request.node.name,
739 test_repo_container=test_repo)
741 test_repo_container=test_repo)
740 request.addfinalizer(backend.cleanup)
742 request.addfinalizer(backend.cleanup)
741 return backend
743 return backend
742
744
743
745
744 @pytest.fixture()
746 @pytest.fixture()
745 def vcsbackend(request, backend_alias, tests_tmp_path, baseapp, test_repo):
747 def vcsbackend(request, backend_alias, tests_tmp_path, baseapp, test_repo):
746 """
748 """
747 Parametrized fixture which represents a single vcs backend implementation.
749 Parametrized fixture which represents a single vcs backend implementation.
748
750
749 See the fixture `backend` for more details. This one implements the same
751 See the fixture `backend` for more details. This one implements the same
750 concept, but on vcs level. So it does not provide model instances etc.
752 concept, but on vcs level. So it does not provide model instances etc.
751
753
752 Parameters are generated dynamically, see :func:`pytest_generate_tests`
754 Parameters are generated dynamically, see :func:`pytest_generate_tests`
753 for how this works.
755 for how this works.
754 """
756 """
755 return vcsbackend_base(request, backend_alias, tests_tmp_path, baseapp, test_repo)
757 return vcsbackend_base(request, backend_alias, tests_tmp_path, baseapp, test_repo)
756
758
757
759
758 @pytest.fixture()
760 @pytest.fixture()
759 def vcsbackend_git(request, tests_tmp_path, baseapp, test_repo):
761 def vcsbackend_git(request, tests_tmp_path, baseapp, test_repo):
760 return vcsbackend_base(request, 'git', tests_tmp_path, baseapp, test_repo)
762 return vcsbackend_base(request, 'git', tests_tmp_path, baseapp, test_repo)
761
763
762
764
763 @pytest.fixture()
765 @pytest.fixture()
764 def vcsbackend_hg(request, tests_tmp_path, baseapp, test_repo):
766 def vcsbackend_hg(request, tests_tmp_path, baseapp, test_repo):
765 return vcsbackend_base(request, 'hg', tests_tmp_path, baseapp, test_repo)
767 return vcsbackend_base(request, 'hg', tests_tmp_path, baseapp, test_repo)
766
768
767
769
768 @pytest.fixture()
770 @pytest.fixture()
769 def vcsbackend_svn(request, tests_tmp_path, baseapp, test_repo):
771 def vcsbackend_svn(request, tests_tmp_path, baseapp, test_repo):
770 return vcsbackend_base(request, 'svn', tests_tmp_path, baseapp, test_repo)
772 return vcsbackend_base(request, 'svn', tests_tmp_path, baseapp, test_repo)
771
773
772
774
773 @pytest.fixture()
775 @pytest.fixture()
774 def vcsbackend_stub(vcsbackend_git):
776 def vcsbackend_stub(vcsbackend_git):
775 """
777 """
776 Use this to express that your test just needs a stub of a vcsbackend.
778 Use this to express that your test just needs a stub of a vcsbackend.
777
779
778 Plan is to eventually implement an in-memory stub to speed tests up.
780 Plan is to eventually implement an in-memory stub to speed tests up.
779 """
781 """
780 return vcsbackend_git
782 return vcsbackend_git
781
783
782
784
783 class VcsBackend(object):
785 class VcsBackend(object):
784 """
786 """
785 Represents the test configuration for one supported vcs backend.
787 Represents the test configuration for one supported vcs backend.
786 """
788 """
787
789
788 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
790 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
789
791
790 def __init__(self, alias, repo_path, test_name, test_repo_container):
792 def __init__(self, alias, repo_path, test_name, test_repo_container):
791 self.alias = alias
793 self.alias = alias
792 self._repo_path = repo_path
794 self._repo_path = repo_path
793 self._cleanup_repos = []
795 self._cleanup_repos = []
794 self._test_name = test_name
796 self._test_name = test_name
795 self._test_repo_container = test_repo_container
797 self._test_repo_container = test_repo_container
796
798
797 def __getitem__(self, key):
799 def __getitem__(self, key):
798 return self._test_repo_container(key, self.alias).scm_instance()
800 return self._test_repo_container(key, self.alias).scm_instance()
799
801
800 @property
802 @property
801 def repo(self):
803 def repo(self):
802 """
804 """
803 Returns the "current" repository. This is the vcs_test repo of the last
805 Returns the "current" repository. This is the vcs_test repo of the last
804 repo which has been created.
806 repo which has been created.
805 """
807 """
806 Repository = get_backend(self.alias)
808 Repository = get_backend(self.alias)
807 return Repository(self._repo_path)
809 return Repository(self._repo_path)
808
810
809 @property
811 @property
810 def backend(self):
812 def backend(self):
811 """
813 """
812 Returns the backend implementation class.
814 Returns the backend implementation class.
813 """
815 """
814 return get_backend(self.alias)
816 return get_backend(self.alias)
815
817
816 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None,
818 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None,
817 bare=False):
819 bare=False):
818 repo_name = self._next_repo_name()
820 repo_name = self._next_repo_name()
819 self._repo_path = get_new_dir(repo_name)
821 self._repo_path = get_new_dir(repo_name)
820 repo_class = get_backend(self.alias)
822 repo_class = get_backend(self.alias)
821 src_url = None
823 src_url = None
822 if _clone_repo:
824 if _clone_repo:
823 src_url = _clone_repo.path
825 src_url = _clone_repo.path
824 repo = repo_class(self._repo_path, create=True, src_url=src_url, bare=bare)
826 repo = repo_class(self._repo_path, create=True, src_url=src_url, bare=bare)
825 self._cleanup_repos.append(repo)
827 self._cleanup_repos.append(repo)
826
828
827 commits = commits or [
829 commits = commits or [
828 {'message': 'Commit %s of %s' % (x, repo_name)}
830 {'message': 'Commit %s of %s' % (x, repo_name)}
829 for x in xrange(number_of_commits)]
831 for x in xrange(number_of_commits)]
830 _add_commits_to_repo(repo, commits)
832 _add_commits_to_repo(repo, commits)
831 return repo
833 return repo
832
834
833 def clone_repo(self, repo):
835 def clone_repo(self, repo):
834 return self.create_repo(_clone_repo=repo)
836 return self.create_repo(_clone_repo=repo)
835
837
836 def cleanup(self):
838 def cleanup(self):
837 for repo in self._cleanup_repos:
839 for repo in self._cleanup_repos:
838 shutil.rmtree(repo.path)
840 shutil.rmtree(repo.path)
839
841
840 def new_repo_path(self):
842 def new_repo_path(self):
841 repo_name = self._next_repo_name()
843 repo_name = self._next_repo_name()
842 self._repo_path = get_new_dir(repo_name)
844 self._repo_path = get_new_dir(repo_name)
843 return self._repo_path
845 return self._repo_path
844
846
845 def _next_repo_name(self):
847 def _next_repo_name(self):
846 return "%s_%s" % (
848 return "%s_%s" % (
847 self.invalid_repo_name.sub('_', self._test_name),
849 self.invalid_repo_name.sub('_', self._test_name),
848 len(self._cleanup_repos))
850 len(self._cleanup_repos))
849
851
850 def add_file(self, repo, filename, content='Test content\n'):
852 def add_file(self, repo, filename, content='Test content\n'):
851 imc = repo.in_memory_commit
853 imc = repo.in_memory_commit
852 imc.add(FileNode(filename, content=content))
854 imc.add(FileNode(filename, content=content))
853 imc.commit(
855 imc.commit(
854 message=u'Automatic commit from vcsbackend fixture',
856 message=u'Automatic commit from vcsbackend fixture',
855 author=u'Automatic <automatic@rhodecode.com>')
857 author=u'Automatic <automatic@rhodecode.com>')
856
858
857 def ensure_file(self, filename, content='Test content\n'):
859 def ensure_file(self, filename, content='Test content\n'):
858 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
860 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
859 self.add_file(self.repo, filename, content)
861 self.add_file(self.repo, filename, content)
860
862
861
863
862 def _add_commits_to_repo(vcs_repo, commits):
864 def _add_commits_to_repo(vcs_repo, commits):
863 commit_ids = {}
865 commit_ids = {}
864 if not commits:
866 if not commits:
865 return commit_ids
867 return commit_ids
866
868
867 imc = vcs_repo.in_memory_commit
869 imc = vcs_repo.in_memory_commit
868 commit = None
870 commit = None
869
871
870 for idx, commit in enumerate(commits):
872 for idx, commit in enumerate(commits):
871 message = unicode(commit.get('message', 'Commit %s' % idx))
873 message = unicode(commit.get('message', 'Commit %s' % idx))
872
874
873 for node in commit.get('added', []):
875 for node in commit.get('added', []):
874 imc.add(FileNode(node.path, content=node.content))
876 imc.add(FileNode(node.path, content=node.content))
875 for node in commit.get('changed', []):
877 for node in commit.get('changed', []):
876 imc.change(FileNode(node.path, content=node.content))
878 imc.change(FileNode(node.path, content=node.content))
877 for node in commit.get('removed', []):
879 for node in commit.get('removed', []):
878 imc.remove(FileNode(node.path))
880 imc.remove(FileNode(node.path))
879
881
880 parents = [
882 parents = [
881 vcs_repo.get_commit(commit_id=commit_ids[p])
883 vcs_repo.get_commit(commit_id=commit_ids[p])
882 for p in commit.get('parents', [])]
884 for p in commit.get('parents', [])]
883
885
884 operations = ('added', 'changed', 'removed')
886 operations = ('added', 'changed', 'removed')
885 if not any((commit.get(o) for o in operations)):
887 if not any((commit.get(o) for o in operations)):
886 imc.add(FileNode('file_%s' % idx, content=message))
888 imc.add(FileNode('file_%s' % idx, content=message))
887
889
888 commit = imc.commit(
890 commit = imc.commit(
889 message=message,
891 message=message,
890 author=unicode(commit.get('author', 'Automatic <automatic@rhodecode.com>')),
892 author=unicode(commit.get('author', 'Automatic <automatic@rhodecode.com>')),
891 date=commit.get('date'),
893 date=commit.get('date'),
892 branch=commit.get('branch'),
894 branch=commit.get('branch'),
893 parents=parents)
895 parents=parents)
894
896
895 commit_ids[commit.message] = commit.raw_id
897 commit_ids[commit.message] = commit.raw_id
896
898
897 return commit_ids
899 return commit_ids
898
900
899
901
900 @pytest.fixture()
902 @pytest.fixture()
901 def reposerver(request):
903 def reposerver(request):
902 """
904 """
903 Allows to serve a backend repository
905 Allows to serve a backend repository
904 """
906 """
905
907
906 repo_server = RepoServer()
908 repo_server = RepoServer()
907 request.addfinalizer(repo_server.cleanup)
909 request.addfinalizer(repo_server.cleanup)
908 return repo_server
910 return repo_server
909
911
910
912
911 class RepoServer(object):
913 class RepoServer(object):
912 """
914 """
913 Utility to serve a local repository for the duration of a test case.
915 Utility to serve a local repository for the duration of a test case.
914
916
915 Supports only Subversion so far.
917 Supports only Subversion so far.
916 """
918 """
917
919
918 url = None
920 url = None
919
921
920 def __init__(self):
922 def __init__(self):
921 self._cleanup_servers = []
923 self._cleanup_servers = []
922
924
923 def serve(self, vcsrepo):
925 def serve(self, vcsrepo):
924 if vcsrepo.alias != 'svn':
926 if vcsrepo.alias != 'svn':
925 raise TypeError("Backend %s not supported" % vcsrepo.alias)
927 raise TypeError("Backend %s not supported" % vcsrepo.alias)
926
928
927 proc = subprocess32.Popen(
929 proc = subprocess32.Popen(
928 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
930 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
929 '--root', vcsrepo.path])
931 '--root', vcsrepo.path])
930 self._cleanup_servers.append(proc)
932 self._cleanup_servers.append(proc)
931 self.url = 'svn://localhost'
933 self.url = 'svn://localhost'
932
934
933 def cleanup(self):
935 def cleanup(self):
934 for proc in self._cleanup_servers:
936 for proc in self._cleanup_servers:
935 proc.terminate()
937 proc.terminate()
936
938
937
939
938 @pytest.fixture()
940 @pytest.fixture()
939 def pr_util(backend, request, config_stub):
941 def pr_util(backend, request, config_stub):
940 """
942 """
941 Utility for tests of models and for functional tests around pull requests.
943 Utility for tests of models and for functional tests around pull requests.
942
944
943 It gives an instance of :class:`PRTestUtility` which provides various
945 It gives an instance of :class:`PRTestUtility` which provides various
944 utility methods around one pull request.
946 utility methods around one pull request.
945
947
946 This fixture uses `backend` and inherits its parameterization.
948 This fixture uses `backend` and inherits its parameterization.
947 """
949 """
948
950
949 util = PRTestUtility(backend)
951 util = PRTestUtility(backend)
950 request.addfinalizer(util.cleanup)
952 request.addfinalizer(util.cleanup)
951
953
952 return util
954 return util
953
955
954
956
955 class PRTestUtility(object):
957 class PRTestUtility(object):
956
958
957 pull_request = None
959 pull_request = None
958 pull_request_id = None
960 pull_request_id = None
959 mergeable_patcher = None
961 mergeable_patcher = None
960 mergeable_mock = None
962 mergeable_mock = None
961 notification_patcher = None
963 notification_patcher = None
962
964
963 def __init__(self, backend):
965 def __init__(self, backend):
964 self.backend = backend
966 self.backend = backend
965
967
966 def create_pull_request(
968 def create_pull_request(
967 self, commits=None, target_head=None, source_head=None,
969 self, commits=None, target_head=None, source_head=None,
968 revisions=None, approved=False, author=None, mergeable=False,
970 revisions=None, approved=False, author=None, mergeable=False,
969 enable_notifications=True, name_suffix=u'', reviewers=None,
971 enable_notifications=True, name_suffix=u'', reviewers=None,
970 title=u"Test", description=u"Description"):
972 title=u"Test", description=u"Description"):
971 self.set_mergeable(mergeable)
973 self.set_mergeable(mergeable)
972 if not enable_notifications:
974 if not enable_notifications:
973 # mock notification side effect
975 # mock notification side effect
974 self.notification_patcher = mock.patch(
976 self.notification_patcher = mock.patch(
975 'rhodecode.model.notification.NotificationModel.create')
977 'rhodecode.model.notification.NotificationModel.create')
976 self.notification_patcher.start()
978 self.notification_patcher.start()
977
979
978 if not self.pull_request:
980 if not self.pull_request:
979 if not commits:
981 if not commits:
980 commits = [
982 commits = [
981 {'message': 'c1'},
983 {'message': 'c1'},
982 {'message': 'c2'},
984 {'message': 'c2'},
983 {'message': 'c3'},
985 {'message': 'c3'},
984 ]
986 ]
985 target_head = 'c1'
987 target_head = 'c1'
986 source_head = 'c2'
988 source_head = 'c2'
987 revisions = ['c2']
989 revisions = ['c2']
988
990
989 self.commit_ids = self.backend.create_master_repo(commits)
991 self.commit_ids = self.backend.create_master_repo(commits)
990 self.target_repository = self.backend.create_repo(
992 self.target_repository = self.backend.create_repo(
991 heads=[target_head], name_suffix=name_suffix)
993 heads=[target_head], name_suffix=name_suffix)
992 self.source_repository = self.backend.create_repo(
994 self.source_repository = self.backend.create_repo(
993 heads=[source_head], name_suffix=name_suffix)
995 heads=[source_head], name_suffix=name_suffix)
994 self.author = author or UserModel().get_by_username(
996 self.author = author or UserModel().get_by_username(
995 TEST_USER_ADMIN_LOGIN)
997 TEST_USER_ADMIN_LOGIN)
996
998
997 model = PullRequestModel()
999 model = PullRequestModel()
998 self.create_parameters = {
1000 self.create_parameters = {
999 'created_by': self.author,
1001 'created_by': self.author,
1000 'source_repo': self.source_repository.repo_name,
1002 'source_repo': self.source_repository.repo_name,
1001 'source_ref': self._default_branch_reference(source_head),
1003 'source_ref': self._default_branch_reference(source_head),
1002 'target_repo': self.target_repository.repo_name,
1004 'target_repo': self.target_repository.repo_name,
1003 'target_ref': self._default_branch_reference(target_head),
1005 'target_ref': self._default_branch_reference(target_head),
1004 'revisions': [self.commit_ids[r] for r in revisions],
1006 'revisions': [self.commit_ids[r] for r in revisions],
1005 'reviewers': reviewers or self._get_reviewers(),
1007 'reviewers': reviewers or self._get_reviewers(),
1006 'title': title,
1008 'title': title,
1007 'description': description,
1009 'description': description,
1008 }
1010 }
1009 self.pull_request = model.create(**self.create_parameters)
1011 self.pull_request = model.create(**self.create_parameters)
1010 assert model.get_versions(self.pull_request) == []
1012 assert model.get_versions(self.pull_request) == []
1011
1013
1012 self.pull_request_id = self.pull_request.pull_request_id
1014 self.pull_request_id = self.pull_request.pull_request_id
1013
1015
1014 if approved:
1016 if approved:
1015 self.approve()
1017 self.approve()
1016
1018
1017 Session().add(self.pull_request)
1019 Session().add(self.pull_request)
1018 Session().commit()
1020 Session().commit()
1019
1021
1020 return self.pull_request
1022 return self.pull_request
1021
1023
1022 def approve(self):
1024 def approve(self):
1023 self.create_status_votes(
1025 self.create_status_votes(
1024 ChangesetStatus.STATUS_APPROVED,
1026 ChangesetStatus.STATUS_APPROVED,
1025 *self.pull_request.reviewers)
1027 *self.pull_request.reviewers)
1026
1028
1027 def close(self):
1029 def close(self):
1028 PullRequestModel().close_pull_request(self.pull_request, self.author)
1030 PullRequestModel().close_pull_request(self.pull_request, self.author)
1029
1031
1030 def _default_branch_reference(self, commit_message):
1032 def _default_branch_reference(self, commit_message):
1031 reference = '%s:%s:%s' % (
1033 reference = '%s:%s:%s' % (
1032 'branch',
1034 'branch',
1033 self.backend.default_branch_name,
1035 self.backend.default_branch_name,
1034 self.commit_ids[commit_message])
1036 self.commit_ids[commit_message])
1035 return reference
1037 return reference
1036
1038
1037 def _get_reviewers(self):
1039 def _get_reviewers(self):
1038 return [
1040 return [
1039 (TEST_USER_REGULAR_LOGIN, ['default1'], False, []),
1041 (TEST_USER_REGULAR_LOGIN, ['default1'], False, []),
1040 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, []),
1042 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, []),
1041 ]
1043 ]
1042
1044
1043 def update_source_repository(self, head=None):
1045 def update_source_repository(self, head=None):
1044 heads = [head or 'c3']
1046 heads = [head or 'c3']
1045 self.backend.pull_heads(self.source_repository, heads=heads)
1047 self.backend.pull_heads(self.source_repository, heads=heads)
1046
1048
1047 def add_one_commit(self, head=None):
1049 def add_one_commit(self, head=None):
1048 self.update_source_repository(head=head)
1050 self.update_source_repository(head=head)
1049 old_commit_ids = set(self.pull_request.revisions)
1051 old_commit_ids = set(self.pull_request.revisions)
1050 PullRequestModel().update_commits(self.pull_request, self.pull_request.author)
1052 PullRequestModel().update_commits(self.pull_request, self.pull_request.author)
1051 commit_ids = set(self.pull_request.revisions)
1053 commit_ids = set(self.pull_request.revisions)
1052 new_commit_ids = commit_ids - old_commit_ids
1054 new_commit_ids = commit_ids - old_commit_ids
1053 assert len(new_commit_ids) == 1
1055 assert len(new_commit_ids) == 1
1054 return new_commit_ids.pop()
1056 return new_commit_ids.pop()
1055
1057
1056 def remove_one_commit(self):
1058 def remove_one_commit(self):
1057 assert len(self.pull_request.revisions) == 2
1059 assert len(self.pull_request.revisions) == 2
1058 source_vcs = self.source_repository.scm_instance()
1060 source_vcs = self.source_repository.scm_instance()
1059 removed_commit_id = source_vcs.commit_ids[-1]
1061 removed_commit_id = source_vcs.commit_ids[-1]
1060
1062
1061 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1063 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1062 # remove the if once that's sorted out.
1064 # remove the if once that's sorted out.
1063 if self.backend.alias == "git":
1065 if self.backend.alias == "git":
1064 kwargs = {'branch_name': self.backend.default_branch_name}
1066 kwargs = {'branch_name': self.backend.default_branch_name}
1065 else:
1067 else:
1066 kwargs = {}
1068 kwargs = {}
1067 source_vcs.strip(removed_commit_id, **kwargs)
1069 source_vcs.strip(removed_commit_id, **kwargs)
1068
1070
1069 PullRequestModel().update_commits(self.pull_request, self.pull_request.author)
1071 PullRequestModel().update_commits(self.pull_request, self.pull_request.author)
1070 assert len(self.pull_request.revisions) == 1
1072 assert len(self.pull_request.revisions) == 1
1071 return removed_commit_id
1073 return removed_commit_id
1072
1074
1073 def create_comment(self, linked_to=None):
1075 def create_comment(self, linked_to=None):
1074 comment = CommentsModel().create(
1076 comment = CommentsModel().create(
1075 text=u"Test comment",
1077 text=u"Test comment",
1076 repo=self.target_repository.repo_name,
1078 repo=self.target_repository.repo_name,
1077 user=self.author,
1079 user=self.author,
1078 pull_request=self.pull_request)
1080 pull_request=self.pull_request)
1079 assert comment.pull_request_version_id is None
1081 assert comment.pull_request_version_id is None
1080
1082
1081 if linked_to:
1083 if linked_to:
1082 PullRequestModel()._link_comments_to_version(linked_to)
1084 PullRequestModel()._link_comments_to_version(linked_to)
1083
1085
1084 return comment
1086 return comment
1085
1087
1086 def create_inline_comment(
1088 def create_inline_comment(
1087 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1089 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1088 comment = CommentsModel().create(
1090 comment = CommentsModel().create(
1089 text=u"Test comment",
1091 text=u"Test comment",
1090 repo=self.target_repository.repo_name,
1092 repo=self.target_repository.repo_name,
1091 user=self.author,
1093 user=self.author,
1092 line_no=line_no,
1094 line_no=line_no,
1093 f_path=file_path,
1095 f_path=file_path,
1094 pull_request=self.pull_request)
1096 pull_request=self.pull_request)
1095 assert comment.pull_request_version_id is None
1097 assert comment.pull_request_version_id is None
1096
1098
1097 if linked_to:
1099 if linked_to:
1098 PullRequestModel()._link_comments_to_version(linked_to)
1100 PullRequestModel()._link_comments_to_version(linked_to)
1099
1101
1100 return comment
1102 return comment
1101
1103
1102 def create_version_of_pull_request(self):
1104 def create_version_of_pull_request(self):
1103 pull_request = self.create_pull_request()
1105 pull_request = self.create_pull_request()
1104 version = PullRequestModel()._create_version_from_snapshot(
1106 version = PullRequestModel()._create_version_from_snapshot(
1105 pull_request)
1107 pull_request)
1106 return version
1108 return version
1107
1109
1108 def create_status_votes(self, status, *reviewers):
1110 def create_status_votes(self, status, *reviewers):
1109 for reviewer in reviewers:
1111 for reviewer in reviewers:
1110 ChangesetStatusModel().set_status(
1112 ChangesetStatusModel().set_status(
1111 repo=self.pull_request.target_repo,
1113 repo=self.pull_request.target_repo,
1112 status=status,
1114 status=status,
1113 user=reviewer.user_id,
1115 user=reviewer.user_id,
1114 pull_request=self.pull_request)
1116 pull_request=self.pull_request)
1115
1117
1116 def set_mergeable(self, value):
1118 def set_mergeable(self, value):
1117 if not self.mergeable_patcher:
1119 if not self.mergeable_patcher:
1118 self.mergeable_patcher = mock.patch.object(
1120 self.mergeable_patcher = mock.patch.object(
1119 VcsSettingsModel, 'get_general_settings')
1121 VcsSettingsModel, 'get_general_settings')
1120 self.mergeable_mock = self.mergeable_patcher.start()
1122 self.mergeable_mock = self.mergeable_patcher.start()
1121 self.mergeable_mock.return_value = {
1123 self.mergeable_mock.return_value = {
1122 'rhodecode_pr_merge_enabled': value}
1124 'rhodecode_pr_merge_enabled': value}
1123
1125
1124 def cleanup(self):
1126 def cleanup(self):
1125 # In case the source repository is already cleaned up, the pull
1127 # In case the source repository is already cleaned up, the pull
1126 # request will already be deleted.
1128 # request will already be deleted.
1127 pull_request = PullRequest().get(self.pull_request_id)
1129 pull_request = PullRequest().get(self.pull_request_id)
1128 if pull_request:
1130 if pull_request:
1129 PullRequestModel().delete(pull_request, pull_request.author)
1131 PullRequestModel().delete(pull_request, pull_request.author)
1130 Session().commit()
1132 Session().commit()
1131
1133
1132 if self.notification_patcher:
1134 if self.notification_patcher:
1133 self.notification_patcher.stop()
1135 self.notification_patcher.stop()
1134
1136
1135 if self.mergeable_patcher:
1137 if self.mergeable_patcher:
1136 self.mergeable_patcher.stop()
1138 self.mergeable_patcher.stop()
1137
1139
1138
1140
1139 @pytest.fixture()
1141 @pytest.fixture()
1140 def user_admin(baseapp):
1142 def user_admin(baseapp):
1141 """
1143 """
1142 Provides the default admin test user as an instance of `db.User`.
1144 Provides the default admin test user as an instance of `db.User`.
1143 """
1145 """
1144 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1146 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1145 return user
1147 return user
1146
1148
1147
1149
1148 @pytest.fixture()
1150 @pytest.fixture()
1149 def user_regular(baseapp):
1151 def user_regular(baseapp):
1150 """
1152 """
1151 Provides the default regular test user as an instance of `db.User`.
1153 Provides the default regular test user as an instance of `db.User`.
1152 """
1154 """
1153 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1155 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1154 return user
1156 return user
1155
1157
1156
1158
1157 @pytest.fixture()
1159 @pytest.fixture()
1158 def user_util(request, db_connection):
1160 def user_util(request, db_connection):
1159 """
1161 """
1160 Provides a wired instance of `UserUtility` with integrated cleanup.
1162 Provides a wired instance of `UserUtility` with integrated cleanup.
1161 """
1163 """
1162 utility = UserUtility(test_name=request.node.name)
1164 utility = UserUtility(test_name=request.node.name)
1163 request.addfinalizer(utility.cleanup)
1165 request.addfinalizer(utility.cleanup)
1164 return utility
1166 return utility
1165
1167
1166
1168
1167 # TODO: johbo: Split this up into utilities per domain or something similar
1169 # TODO: johbo: Split this up into utilities per domain or something similar
1168 class UserUtility(object):
1170 class UserUtility(object):
1169
1171
1170 def __init__(self, test_name="test"):
1172 def __init__(self, test_name="test"):
1171 self._test_name = self._sanitize_name(test_name)
1173 self._test_name = self._sanitize_name(test_name)
1172 self.fixture = Fixture()
1174 self.fixture = Fixture()
1173 self.repo_group_ids = []
1175 self.repo_group_ids = []
1174 self.repos_ids = []
1176 self.repos_ids = []
1175 self.user_ids = []
1177 self.user_ids = []
1176 self.user_group_ids = []
1178 self.user_group_ids = []
1177 self.user_repo_permission_ids = []
1179 self.user_repo_permission_ids = []
1178 self.user_group_repo_permission_ids = []
1180 self.user_group_repo_permission_ids = []
1179 self.user_repo_group_permission_ids = []
1181 self.user_repo_group_permission_ids = []
1180 self.user_group_repo_group_permission_ids = []
1182 self.user_group_repo_group_permission_ids = []
1181 self.user_user_group_permission_ids = []
1183 self.user_user_group_permission_ids = []
1182 self.user_group_user_group_permission_ids = []
1184 self.user_group_user_group_permission_ids = []
1183 self.user_permissions = []
1185 self.user_permissions = []
1184
1186
1185 def _sanitize_name(self, name):
1187 def _sanitize_name(self, name):
1186 for char in ['[', ']']:
1188 for char in ['[', ']']:
1187 name = name.replace(char, '_')
1189 name = name.replace(char, '_')
1188 return name
1190 return name
1189
1191
1190 def create_repo_group(
1192 def create_repo_group(
1191 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1193 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1192 group_name = "{prefix}_repogroup_{count}".format(
1194 group_name = "{prefix}_repogroup_{count}".format(
1193 prefix=self._test_name,
1195 prefix=self._test_name,
1194 count=len(self.repo_group_ids))
1196 count=len(self.repo_group_ids))
1195 repo_group = self.fixture.create_repo_group(
1197 repo_group = self.fixture.create_repo_group(
1196 group_name, cur_user=owner)
1198 group_name, cur_user=owner)
1197 if auto_cleanup:
1199 if auto_cleanup:
1198 self.repo_group_ids.append(repo_group.group_id)
1200 self.repo_group_ids.append(repo_group.group_id)
1199 return repo_group
1201 return repo_group
1200
1202
1201 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1203 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1202 auto_cleanup=True, repo_type='hg', bare=False):
1204 auto_cleanup=True, repo_type='hg', bare=False):
1203 repo_name = "{prefix}_repository_{count}".format(
1205 repo_name = "{prefix}_repository_{count}".format(
1204 prefix=self._test_name,
1206 prefix=self._test_name,
1205 count=len(self.repos_ids))
1207 count=len(self.repos_ids))
1206
1208
1207 repository = self.fixture.create_repo(
1209 repository = self.fixture.create_repo(
1208 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type, bare=bare)
1210 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type, bare=bare)
1209 if auto_cleanup:
1211 if auto_cleanup:
1210 self.repos_ids.append(repository.repo_id)
1212 self.repos_ids.append(repository.repo_id)
1211 return repository
1213 return repository
1212
1214
1213 def create_user(self, auto_cleanup=True, **kwargs):
1215 def create_user(self, auto_cleanup=True, **kwargs):
1214 user_name = "{prefix}_user_{count}".format(
1216 user_name = "{prefix}_user_{count}".format(
1215 prefix=self._test_name,
1217 prefix=self._test_name,
1216 count=len(self.user_ids))
1218 count=len(self.user_ids))
1217 user = self.fixture.create_user(user_name, **kwargs)
1219 user = self.fixture.create_user(user_name, **kwargs)
1218 if auto_cleanup:
1220 if auto_cleanup:
1219 self.user_ids.append(user.user_id)
1221 self.user_ids.append(user.user_id)
1220 return user
1222 return user
1221
1223
1222 def create_additional_user_email(self, user, email):
1224 def create_additional_user_email(self, user, email):
1223 uem = self.fixture.create_additional_user_email(user=user, email=email)
1225 uem = self.fixture.create_additional_user_email(user=user, email=email)
1224 return uem
1226 return uem
1225
1227
1226 def create_user_with_group(self):
1228 def create_user_with_group(self):
1227 user = self.create_user()
1229 user = self.create_user()
1228 user_group = self.create_user_group(members=[user])
1230 user_group = self.create_user_group(members=[user])
1229 return user, user_group
1231 return user, user_group
1230
1232
1231 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1233 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1232 auto_cleanup=True, **kwargs):
1234 auto_cleanup=True, **kwargs):
1233 group_name = "{prefix}_usergroup_{count}".format(
1235 group_name = "{prefix}_usergroup_{count}".format(
1234 prefix=self._test_name,
1236 prefix=self._test_name,
1235 count=len(self.user_group_ids))
1237 count=len(self.user_group_ids))
1236 user_group = self.fixture.create_user_group(
1238 user_group = self.fixture.create_user_group(
1237 group_name, cur_user=owner, **kwargs)
1239 group_name, cur_user=owner, **kwargs)
1238
1240
1239 if auto_cleanup:
1241 if auto_cleanup:
1240 self.user_group_ids.append(user_group.users_group_id)
1242 self.user_group_ids.append(user_group.users_group_id)
1241 if members:
1243 if members:
1242 for user in members:
1244 for user in members:
1243 UserGroupModel().add_user_to_group(user_group, user)
1245 UserGroupModel().add_user_to_group(user_group, user)
1244 return user_group
1246 return user_group
1245
1247
1246 def grant_user_permission(self, user_name, permission_name):
1248 def grant_user_permission(self, user_name, permission_name):
1247 self.inherit_default_user_permissions(user_name, False)
1249 self.inherit_default_user_permissions(user_name, False)
1248 self.user_permissions.append((user_name, permission_name))
1250 self.user_permissions.append((user_name, permission_name))
1249
1251
1250 def grant_user_permission_to_repo_group(
1252 def grant_user_permission_to_repo_group(
1251 self, repo_group, user, permission_name):
1253 self, repo_group, user, permission_name):
1252 permission = RepoGroupModel().grant_user_permission(
1254 permission = RepoGroupModel().grant_user_permission(
1253 repo_group, user, permission_name)
1255 repo_group, user, permission_name)
1254 self.user_repo_group_permission_ids.append(
1256 self.user_repo_group_permission_ids.append(
1255 (repo_group.group_id, user.user_id))
1257 (repo_group.group_id, user.user_id))
1256 return permission
1258 return permission
1257
1259
1258 def grant_user_group_permission_to_repo_group(
1260 def grant_user_group_permission_to_repo_group(
1259 self, repo_group, user_group, permission_name):
1261 self, repo_group, user_group, permission_name):
1260 permission = RepoGroupModel().grant_user_group_permission(
1262 permission = RepoGroupModel().grant_user_group_permission(
1261 repo_group, user_group, permission_name)
1263 repo_group, user_group, permission_name)
1262 self.user_group_repo_group_permission_ids.append(
1264 self.user_group_repo_group_permission_ids.append(
1263 (repo_group.group_id, user_group.users_group_id))
1265 (repo_group.group_id, user_group.users_group_id))
1264 return permission
1266 return permission
1265
1267
1266 def grant_user_permission_to_repo(
1268 def grant_user_permission_to_repo(
1267 self, repo, user, permission_name):
1269 self, repo, user, permission_name):
1268 permission = RepoModel().grant_user_permission(
1270 permission = RepoModel().grant_user_permission(
1269 repo, user, permission_name)
1271 repo, user, permission_name)
1270 self.user_repo_permission_ids.append(
1272 self.user_repo_permission_ids.append(
1271 (repo.repo_id, user.user_id))
1273 (repo.repo_id, user.user_id))
1272 return permission
1274 return permission
1273
1275
1274 def grant_user_group_permission_to_repo(
1276 def grant_user_group_permission_to_repo(
1275 self, repo, user_group, permission_name):
1277 self, repo, user_group, permission_name):
1276 permission = RepoModel().grant_user_group_permission(
1278 permission = RepoModel().grant_user_group_permission(
1277 repo, user_group, permission_name)
1279 repo, user_group, permission_name)
1278 self.user_group_repo_permission_ids.append(
1280 self.user_group_repo_permission_ids.append(
1279 (repo.repo_id, user_group.users_group_id))
1281 (repo.repo_id, user_group.users_group_id))
1280 return permission
1282 return permission
1281
1283
1282 def grant_user_permission_to_user_group(
1284 def grant_user_permission_to_user_group(
1283 self, target_user_group, user, permission_name):
1285 self, target_user_group, user, permission_name):
1284 permission = UserGroupModel().grant_user_permission(
1286 permission = UserGroupModel().grant_user_permission(
1285 target_user_group, user, permission_name)
1287 target_user_group, user, permission_name)
1286 self.user_user_group_permission_ids.append(
1288 self.user_user_group_permission_ids.append(
1287 (target_user_group.users_group_id, user.user_id))
1289 (target_user_group.users_group_id, user.user_id))
1288 return permission
1290 return permission
1289
1291
1290 def grant_user_group_permission_to_user_group(
1292 def grant_user_group_permission_to_user_group(
1291 self, target_user_group, user_group, permission_name):
1293 self, target_user_group, user_group, permission_name):
1292 permission = UserGroupModel().grant_user_group_permission(
1294 permission = UserGroupModel().grant_user_group_permission(
1293 target_user_group, user_group, permission_name)
1295 target_user_group, user_group, permission_name)
1294 self.user_group_user_group_permission_ids.append(
1296 self.user_group_user_group_permission_ids.append(
1295 (target_user_group.users_group_id, user_group.users_group_id))
1297 (target_user_group.users_group_id, user_group.users_group_id))
1296 return permission
1298 return permission
1297
1299
1298 def revoke_user_permission(self, user_name, permission_name):
1300 def revoke_user_permission(self, user_name, permission_name):
1299 self.inherit_default_user_permissions(user_name, True)
1301 self.inherit_default_user_permissions(user_name, True)
1300 UserModel().revoke_perm(user_name, permission_name)
1302 UserModel().revoke_perm(user_name, permission_name)
1301
1303
1302 def inherit_default_user_permissions(self, user_name, value):
1304 def inherit_default_user_permissions(self, user_name, value):
1303 user = UserModel().get_by_username(user_name)
1305 user = UserModel().get_by_username(user_name)
1304 user.inherit_default_permissions = value
1306 user.inherit_default_permissions = value
1305 Session().add(user)
1307 Session().add(user)
1306 Session().commit()
1308 Session().commit()
1307
1309
1308 def cleanup(self):
1310 def cleanup(self):
1309 self._cleanup_permissions()
1311 self._cleanup_permissions()
1310 self._cleanup_repos()
1312 self._cleanup_repos()
1311 self._cleanup_repo_groups()
1313 self._cleanup_repo_groups()
1312 self._cleanup_user_groups()
1314 self._cleanup_user_groups()
1313 self._cleanup_users()
1315 self._cleanup_users()
1314
1316
1315 def _cleanup_permissions(self):
1317 def _cleanup_permissions(self):
1316 if self.user_permissions:
1318 if self.user_permissions:
1317 for user_name, permission_name in self.user_permissions:
1319 for user_name, permission_name in self.user_permissions:
1318 self.revoke_user_permission(user_name, permission_name)
1320 self.revoke_user_permission(user_name, permission_name)
1319
1321
1320 for permission in self.user_repo_permission_ids:
1322 for permission in self.user_repo_permission_ids:
1321 RepoModel().revoke_user_permission(*permission)
1323 RepoModel().revoke_user_permission(*permission)
1322
1324
1323 for permission in self.user_group_repo_permission_ids:
1325 for permission in self.user_group_repo_permission_ids:
1324 RepoModel().revoke_user_group_permission(*permission)
1326 RepoModel().revoke_user_group_permission(*permission)
1325
1327
1326 for permission in self.user_repo_group_permission_ids:
1328 for permission in self.user_repo_group_permission_ids:
1327 RepoGroupModel().revoke_user_permission(*permission)
1329 RepoGroupModel().revoke_user_permission(*permission)
1328
1330
1329 for permission in self.user_group_repo_group_permission_ids:
1331 for permission in self.user_group_repo_group_permission_ids:
1330 RepoGroupModel().revoke_user_group_permission(*permission)
1332 RepoGroupModel().revoke_user_group_permission(*permission)
1331
1333
1332 for permission in self.user_user_group_permission_ids:
1334 for permission in self.user_user_group_permission_ids:
1333 UserGroupModel().revoke_user_permission(*permission)
1335 UserGroupModel().revoke_user_permission(*permission)
1334
1336
1335 for permission in self.user_group_user_group_permission_ids:
1337 for permission in self.user_group_user_group_permission_ids:
1336 UserGroupModel().revoke_user_group_permission(*permission)
1338 UserGroupModel().revoke_user_group_permission(*permission)
1337
1339
1338 def _cleanup_repo_groups(self):
1340 def _cleanup_repo_groups(self):
1339 def _repo_group_compare(first_group_id, second_group_id):
1341 def _repo_group_compare(first_group_id, second_group_id):
1340 """
1342 """
1341 Gives higher priority to the groups with the most complex paths
1343 Gives higher priority to the groups with the most complex paths
1342 """
1344 """
1343 first_group = RepoGroup.get(first_group_id)
1345 first_group = RepoGroup.get(first_group_id)
1344 second_group = RepoGroup.get(second_group_id)
1346 second_group = RepoGroup.get(second_group_id)
1345 first_group_parts = (
1347 first_group_parts = (
1346 len(first_group.group_name.split('/')) if first_group else 0)
1348 len(first_group.group_name.split('/')) if first_group else 0)
1347 second_group_parts = (
1349 second_group_parts = (
1348 len(second_group.group_name.split('/')) if second_group else 0)
1350 len(second_group.group_name.split('/')) if second_group else 0)
1349 return cmp(second_group_parts, first_group_parts)
1351 return cmp(second_group_parts, first_group_parts)
1350
1352
1351 sorted_repo_group_ids = sorted(
1353 sorted_repo_group_ids = sorted(
1352 self.repo_group_ids, cmp=_repo_group_compare)
1354 self.repo_group_ids, cmp=_repo_group_compare)
1353 for repo_group_id in sorted_repo_group_ids:
1355 for repo_group_id in sorted_repo_group_ids:
1354 self.fixture.destroy_repo_group(repo_group_id)
1356 self.fixture.destroy_repo_group(repo_group_id)
1355
1357
1356 def _cleanup_repos(self):
1358 def _cleanup_repos(self):
1357 sorted_repos_ids = sorted(self.repos_ids)
1359 sorted_repos_ids = sorted(self.repos_ids)
1358 for repo_id in sorted_repos_ids:
1360 for repo_id in sorted_repos_ids:
1359 self.fixture.destroy_repo(repo_id)
1361 self.fixture.destroy_repo(repo_id)
1360
1362
1361 def _cleanup_user_groups(self):
1363 def _cleanup_user_groups(self):
1362 def _user_group_compare(first_group_id, second_group_id):
1364 def _user_group_compare(first_group_id, second_group_id):
1363 """
1365 """
1364 Gives higher priority to the groups with the most complex paths
1366 Gives higher priority to the groups with the most complex paths
1365 """
1367 """
1366 first_group = UserGroup.get(first_group_id)
1368 first_group = UserGroup.get(first_group_id)
1367 second_group = UserGroup.get(second_group_id)
1369 second_group = UserGroup.get(second_group_id)
1368 first_group_parts = (
1370 first_group_parts = (
1369 len(first_group.users_group_name.split('/'))
1371 len(first_group.users_group_name.split('/'))
1370 if first_group else 0)
1372 if first_group else 0)
1371 second_group_parts = (
1373 second_group_parts = (
1372 len(second_group.users_group_name.split('/'))
1374 len(second_group.users_group_name.split('/'))
1373 if second_group else 0)
1375 if second_group else 0)
1374 return cmp(second_group_parts, first_group_parts)
1376 return cmp(second_group_parts, first_group_parts)
1375
1377
1376 sorted_user_group_ids = sorted(
1378 sorted_user_group_ids = sorted(
1377 self.user_group_ids, cmp=_user_group_compare)
1379 self.user_group_ids, cmp=_user_group_compare)
1378 for user_group_id in sorted_user_group_ids:
1380 for user_group_id in sorted_user_group_ids:
1379 self.fixture.destroy_user_group(user_group_id)
1381 self.fixture.destroy_user_group(user_group_id)
1380
1382
1381 def _cleanup_users(self):
1383 def _cleanup_users(self):
1382 for user_id in self.user_ids:
1384 for user_id in self.user_ids:
1383 self.fixture.destroy_user(user_id)
1385 self.fixture.destroy_user(user_id)
1384
1386
1385
1387
1386 # TODO: Think about moving this into a pytest-pyro package and make it a
1388 # TODO: Think about moving this into a pytest-pyro package and make it a
1387 # pytest plugin
1389 # pytest plugin
1388 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1390 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1389 def pytest_runtest_makereport(item, call):
1391 def pytest_runtest_makereport(item, call):
1390 """
1392 """
1391 Adding the remote traceback if the exception has this information.
1393 Adding the remote traceback if the exception has this information.
1392
1394
1393 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1395 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1394 to the exception instance.
1396 to the exception instance.
1395 """
1397 """
1396 outcome = yield
1398 outcome = yield
1397 report = outcome.get_result()
1399 report = outcome.get_result()
1398 if call.excinfo:
1400 if call.excinfo:
1399 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1401 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1400
1402
1401
1403
1402 def _add_vcsserver_remote_traceback(report, exc):
1404 def _add_vcsserver_remote_traceback(report, exc):
1403 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1405 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1404
1406
1405 if vcsserver_traceback:
1407 if vcsserver_traceback:
1406 section = 'VCSServer remote traceback ' + report.when
1408 section = 'VCSServer remote traceback ' + report.when
1407 report.sections.append((section, vcsserver_traceback))
1409 report.sections.append((section, vcsserver_traceback))
1408
1410
1409
1411
1410 @pytest.fixture(scope='session')
1412 @pytest.fixture(scope='session')
1411 def testrun():
1413 def testrun():
1412 return {
1414 return {
1413 'uuid': uuid.uuid4(),
1415 'uuid': uuid.uuid4(),
1414 'start': datetime.datetime.utcnow().isoformat(),
1416 'start': datetime.datetime.utcnow().isoformat(),
1415 'timestamp': int(time.time()),
1417 'timestamp': int(time.time()),
1416 }
1418 }
1417
1419
1418
1420
1419 class AppenlightClient(object):
1421 class AppenlightClient(object):
1420
1422
1421 url_template = '{url}?protocol_version=0.5'
1423 url_template = '{url}?protocol_version=0.5'
1422
1424
1423 def __init__(
1425 def __init__(
1424 self, url, api_key, add_server=True, add_timestamp=True,
1426 self, url, api_key, add_server=True, add_timestamp=True,
1425 namespace=None, request=None, testrun=None):
1427 namespace=None, request=None, testrun=None):
1426 self.url = self.url_template.format(url=url)
1428 self.url = self.url_template.format(url=url)
1427 self.api_key = api_key
1429 self.api_key = api_key
1428 self.add_server = add_server
1430 self.add_server = add_server
1429 self.add_timestamp = add_timestamp
1431 self.add_timestamp = add_timestamp
1430 self.namespace = namespace
1432 self.namespace = namespace
1431 self.request = request
1433 self.request = request
1432 self.server = socket.getfqdn(socket.gethostname())
1434 self.server = socket.getfqdn(socket.gethostname())
1433 self.tags_before = {}
1435 self.tags_before = {}
1434 self.tags_after = {}
1436 self.tags_after = {}
1435 self.stats = []
1437 self.stats = []
1436 self.testrun = testrun or {}
1438 self.testrun = testrun or {}
1437
1439
1438 def tag_before(self, tag, value):
1440 def tag_before(self, tag, value):
1439 self.tags_before[tag] = value
1441 self.tags_before[tag] = value
1440
1442
1441 def tag_after(self, tag, value):
1443 def tag_after(self, tag, value):
1442 self.tags_after[tag] = value
1444 self.tags_after[tag] = value
1443
1445
1444 def collect(self, data):
1446 def collect(self, data):
1445 if self.add_server:
1447 if self.add_server:
1446 data.setdefault('server', self.server)
1448 data.setdefault('server', self.server)
1447 if self.add_timestamp:
1449 if self.add_timestamp:
1448 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1450 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1449 if self.namespace:
1451 if self.namespace:
1450 data.setdefault('namespace', self.namespace)
1452 data.setdefault('namespace', self.namespace)
1451 if self.request:
1453 if self.request:
1452 data.setdefault('request', self.request)
1454 data.setdefault('request', self.request)
1453 self.stats.append(data)
1455 self.stats.append(data)
1454
1456
1455 def send_stats(self):
1457 def send_stats(self):
1456 tags = [
1458 tags = [
1457 ('testrun', self.request),
1459 ('testrun', self.request),
1458 ('testrun.start', self.testrun['start']),
1460 ('testrun.start', self.testrun['start']),
1459 ('testrun.timestamp', self.testrun['timestamp']),
1461 ('testrun.timestamp', self.testrun['timestamp']),
1460 ('test', self.namespace),
1462 ('test', self.namespace),
1461 ]
1463 ]
1462 for key, value in self.tags_before.items():
1464 for key, value in self.tags_before.items():
1463 tags.append((key + '.before', value))
1465 tags.append((key + '.before', value))
1464 try:
1466 try:
1465 delta = self.tags_after[key] - value
1467 delta = self.tags_after[key] - value
1466 tags.append((key + '.delta', delta))
1468 tags.append((key + '.delta', delta))
1467 except Exception:
1469 except Exception:
1468 pass
1470 pass
1469 for key, value in self.tags_after.items():
1471 for key, value in self.tags_after.items():
1470 tags.append((key + '.after', value))
1472 tags.append((key + '.after', value))
1471 self.collect({
1473 self.collect({
1472 'message': "Collected tags",
1474 'message': "Collected tags",
1473 'tags': tags,
1475 'tags': tags,
1474 })
1476 })
1475
1477
1476 response = requests.post(
1478 response = requests.post(
1477 self.url,
1479 self.url,
1478 headers={
1480 headers={
1479 'X-appenlight-api-key': self.api_key},
1481 'X-appenlight-api-key': self.api_key},
1480 json=self.stats,
1482 json=self.stats,
1481 )
1483 )
1482
1484
1483 if not response.status_code == 200:
1485 if not response.status_code == 200:
1484 pprint.pprint(self.stats)
1486 pprint.pprint(self.stats)
1485 print(response.headers)
1487 print(response.headers)
1486 print(response.text)
1488 print(response.text)
1487 raise Exception('Sending to appenlight failed')
1489 raise Exception('Sending to appenlight failed')
1488
1490
1489
1491
1490 @pytest.fixture()
1492 @pytest.fixture()
1491 def gist_util(request, db_connection):
1493 def gist_util(request, db_connection):
1492 """
1494 """
1493 Provides a wired instance of `GistUtility` with integrated cleanup.
1495 Provides a wired instance of `GistUtility` with integrated cleanup.
1494 """
1496 """
1495 utility = GistUtility()
1497 utility = GistUtility()
1496 request.addfinalizer(utility.cleanup)
1498 request.addfinalizer(utility.cleanup)
1497 return utility
1499 return utility
1498
1500
1499
1501
1500 class GistUtility(object):
1502 class GistUtility(object):
1501 def __init__(self):
1503 def __init__(self):
1502 self.fixture = Fixture()
1504 self.fixture = Fixture()
1503 self.gist_ids = []
1505 self.gist_ids = []
1504
1506
1505 def create_gist(self, **kwargs):
1507 def create_gist(self, **kwargs):
1506 gist = self.fixture.create_gist(**kwargs)
1508 gist = self.fixture.create_gist(**kwargs)
1507 self.gist_ids.append(gist.gist_id)
1509 self.gist_ids.append(gist.gist_id)
1508 return gist
1510 return gist
1509
1511
1510 def cleanup(self):
1512 def cleanup(self):
1511 for id_ in self.gist_ids:
1513 for id_ in self.gist_ids:
1512 self.fixture.destroy_gists(str(id_))
1514 self.fixture.destroy_gists(str(id_))
1513
1515
1514
1516
1515 @pytest.fixture()
1517 @pytest.fixture()
1516 def enabled_backends(request):
1518 def enabled_backends(request):
1517 backends = request.config.option.backends
1519 backends = request.config.option.backends
1518 return backends[:]
1520 return backends[:]
1519
1521
1520
1522
1521 @pytest.fixture()
1523 @pytest.fixture()
1522 def settings_util(request, db_connection):
1524 def settings_util(request, db_connection):
1523 """
1525 """
1524 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1526 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1525 """
1527 """
1526 utility = SettingsUtility()
1528 utility = SettingsUtility()
1527 request.addfinalizer(utility.cleanup)
1529 request.addfinalizer(utility.cleanup)
1528 return utility
1530 return utility
1529
1531
1530
1532
1531 class SettingsUtility(object):
1533 class SettingsUtility(object):
1532 def __init__(self):
1534 def __init__(self):
1533 self.rhodecode_ui_ids = []
1535 self.rhodecode_ui_ids = []
1534 self.rhodecode_setting_ids = []
1536 self.rhodecode_setting_ids = []
1535 self.repo_rhodecode_ui_ids = []
1537 self.repo_rhodecode_ui_ids = []
1536 self.repo_rhodecode_setting_ids = []
1538 self.repo_rhodecode_setting_ids = []
1537
1539
1538 def create_repo_rhodecode_ui(
1540 def create_repo_rhodecode_ui(
1539 self, repo, section, value, key=None, active=True, cleanup=True):
1541 self, repo, section, value, key=None, active=True, cleanup=True):
1540 key = key or hashlib.sha1(
1542 key = key or hashlib.sha1(
1541 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1543 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1542
1544
1543 setting = RepoRhodeCodeUi()
1545 setting = RepoRhodeCodeUi()
1544 setting.repository_id = repo.repo_id
1546 setting.repository_id = repo.repo_id
1545 setting.ui_section = section
1547 setting.ui_section = section
1546 setting.ui_value = value
1548 setting.ui_value = value
1547 setting.ui_key = key
1549 setting.ui_key = key
1548 setting.ui_active = active
1550 setting.ui_active = active
1549 Session().add(setting)
1551 Session().add(setting)
1550 Session().commit()
1552 Session().commit()
1551
1553
1552 if cleanup:
1554 if cleanup:
1553 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1555 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1554 return setting
1556 return setting
1555
1557
1556 def create_rhodecode_ui(
1558 def create_rhodecode_ui(
1557 self, section, value, key=None, active=True, cleanup=True):
1559 self, section, value, key=None, active=True, cleanup=True):
1558 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1560 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1559
1561
1560 setting = RhodeCodeUi()
1562 setting = RhodeCodeUi()
1561 setting.ui_section = section
1563 setting.ui_section = section
1562 setting.ui_value = value
1564 setting.ui_value = value
1563 setting.ui_key = key
1565 setting.ui_key = key
1564 setting.ui_active = active
1566 setting.ui_active = active
1565 Session().add(setting)
1567 Session().add(setting)
1566 Session().commit()
1568 Session().commit()
1567
1569
1568 if cleanup:
1570 if cleanup:
1569 self.rhodecode_ui_ids.append(setting.ui_id)
1571 self.rhodecode_ui_ids.append(setting.ui_id)
1570 return setting
1572 return setting
1571
1573
1572 def create_repo_rhodecode_setting(
1574 def create_repo_rhodecode_setting(
1573 self, repo, name, value, type_, cleanup=True):
1575 self, repo, name, value, type_, cleanup=True):
1574 setting = RepoRhodeCodeSetting(
1576 setting = RepoRhodeCodeSetting(
1575 repo.repo_id, key=name, val=value, type=type_)
1577 repo.repo_id, key=name, val=value, type=type_)
1576 Session().add(setting)
1578 Session().add(setting)
1577 Session().commit()
1579 Session().commit()
1578
1580
1579 if cleanup:
1581 if cleanup:
1580 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1582 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1581 return setting
1583 return setting
1582
1584
1583 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1585 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1584 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1586 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1585 Session().add(setting)
1587 Session().add(setting)
1586 Session().commit()
1588 Session().commit()
1587
1589
1588 if cleanup:
1590 if cleanup:
1589 self.rhodecode_setting_ids.append(setting.app_settings_id)
1591 self.rhodecode_setting_ids.append(setting.app_settings_id)
1590
1592
1591 return setting
1593 return setting
1592
1594
1593 def cleanup(self):
1595 def cleanup(self):
1594 for id_ in self.rhodecode_ui_ids:
1596 for id_ in self.rhodecode_ui_ids:
1595 setting = RhodeCodeUi.get(id_)
1597 setting = RhodeCodeUi.get(id_)
1596 Session().delete(setting)
1598 Session().delete(setting)
1597
1599
1598 for id_ in self.rhodecode_setting_ids:
1600 for id_ in self.rhodecode_setting_ids:
1599 setting = RhodeCodeSetting.get(id_)
1601 setting = RhodeCodeSetting.get(id_)
1600 Session().delete(setting)
1602 Session().delete(setting)
1601
1603
1602 for id_ in self.repo_rhodecode_ui_ids:
1604 for id_ in self.repo_rhodecode_ui_ids:
1603 setting = RepoRhodeCodeUi.get(id_)
1605 setting = RepoRhodeCodeUi.get(id_)
1604 Session().delete(setting)
1606 Session().delete(setting)
1605
1607
1606 for id_ in self.repo_rhodecode_setting_ids:
1608 for id_ in self.repo_rhodecode_setting_ids:
1607 setting = RepoRhodeCodeSetting.get(id_)
1609 setting = RepoRhodeCodeSetting.get(id_)
1608 Session().delete(setting)
1610 Session().delete(setting)
1609
1611
1610 Session().commit()
1612 Session().commit()
1611
1613
1612
1614
1613 @pytest.fixture()
1615 @pytest.fixture()
1614 def no_notifications(request):
1616 def no_notifications(request):
1615 notification_patcher = mock.patch(
1617 notification_patcher = mock.patch(
1616 'rhodecode.model.notification.NotificationModel.create')
1618 'rhodecode.model.notification.NotificationModel.create')
1617 notification_patcher.start()
1619 notification_patcher.start()
1618 request.addfinalizer(notification_patcher.stop)
1620 request.addfinalizer(notification_patcher.stop)
1619
1621
1620
1622
1621 @pytest.fixture(scope='session')
1623 @pytest.fixture(scope='session')
1622 def repeat(request):
1624 def repeat(request):
1623 """
1625 """
1624 The number of repetitions is based on this fixture.
1626 The number of repetitions is based on this fixture.
1625
1627
1626 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1628 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1627 tests are not too slow in our default test suite.
1629 tests are not too slow in our default test suite.
1628 """
1630 """
1629 return request.config.getoption('--repeat')
1631 return request.config.getoption('--repeat')
1630
1632
1631
1633
1632 @pytest.fixture()
1634 @pytest.fixture()
1633 def rhodecode_fixtures():
1635 def rhodecode_fixtures():
1634 return Fixture()
1636 return Fixture()
1635
1637
1636
1638
1637 @pytest.fixture()
1639 @pytest.fixture()
1638 def context_stub():
1640 def context_stub():
1639 """
1641 """
1640 Stub context object.
1642 Stub context object.
1641 """
1643 """
1642 context = pyramid.testing.DummyResource()
1644 context = pyramid.testing.DummyResource()
1643 return context
1645 return context
1644
1646
1645
1647
1646 @pytest.fixture()
1648 @pytest.fixture()
1647 def request_stub():
1649 def request_stub():
1648 """
1650 """
1649 Stub request object.
1651 Stub request object.
1650 """
1652 """
1651 from rhodecode.lib.base import bootstrap_request
1653 from rhodecode.lib.base import bootstrap_request
1652 request = bootstrap_request(scheme='https')
1654 request = bootstrap_request(scheme='https')
1653 return request
1655 return request
1654
1656
1655
1657
1656 @pytest.fixture()
1658 @pytest.fixture()
1657 def config_stub(request, request_stub):
1659 def config_stub(request, request_stub):
1658 """
1660 """
1659 Set up pyramid.testing and return the Configurator.
1661 Set up pyramid.testing and return the Configurator.
1660 """
1662 """
1661 from rhodecode.lib.base import bootstrap_config
1663 from rhodecode.lib.base import bootstrap_config
1662 config = bootstrap_config(request=request_stub)
1664 config = bootstrap_config(request=request_stub)
1663
1665
1664 @request.addfinalizer
1666 @request.addfinalizer
1665 def cleanup():
1667 def cleanup():
1666 pyramid.testing.tearDown()
1668 pyramid.testing.tearDown()
1667
1669
1668 return config
1670 return config
1669
1671
1670
1672
1671 @pytest.fixture()
1673 @pytest.fixture()
1672 def StubIntegrationType():
1674 def StubIntegrationType():
1673 class _StubIntegrationType(IntegrationTypeBase):
1675 class _StubIntegrationType(IntegrationTypeBase):
1674 """ Test integration type class """
1676 """ Test integration type class """
1675
1677
1676 key = 'test'
1678 key = 'test'
1677 display_name = 'Test integration type'
1679 display_name = 'Test integration type'
1678 description = 'A test integration type for testing'
1680 description = 'A test integration type for testing'
1679
1681
1680 @classmethod
1682 @classmethod
1681 def icon(cls):
1683 def icon(cls):
1682 return 'test_icon_html_image'
1684 return 'test_icon_html_image'
1683
1685
1684 def __init__(self, settings):
1686 def __init__(self, settings):
1685 super(_StubIntegrationType, self).__init__(settings)
1687 super(_StubIntegrationType, self).__init__(settings)
1686 self.sent_events = [] # for testing
1688 self.sent_events = [] # for testing
1687
1689
1688 def send_event(self, event):
1690 def send_event(self, event):
1689 self.sent_events.append(event)
1691 self.sent_events.append(event)
1690
1692
1691 def settings_schema(self):
1693 def settings_schema(self):
1692 class SettingsSchema(colander.Schema):
1694 class SettingsSchema(colander.Schema):
1693 test_string_field = colander.SchemaNode(
1695 test_string_field = colander.SchemaNode(
1694 colander.String(),
1696 colander.String(),
1695 missing=colander.required,
1697 missing=colander.required,
1696 title='test string field',
1698 title='test string field',
1697 )
1699 )
1698 test_int_field = colander.SchemaNode(
1700 test_int_field = colander.SchemaNode(
1699 colander.Int(),
1701 colander.Int(),
1700 title='some integer setting',
1702 title='some integer setting',
1701 )
1703 )
1702 return SettingsSchema()
1704 return SettingsSchema()
1703
1705
1704
1706
1705 integration_type_registry.register_integration_type(_StubIntegrationType)
1707 integration_type_registry.register_integration_type(_StubIntegrationType)
1706 return _StubIntegrationType
1708 return _StubIntegrationType
1707
1709
1708 @pytest.fixture()
1710 @pytest.fixture()
1709 def stub_integration_settings():
1711 def stub_integration_settings():
1710 return {
1712 return {
1711 'test_string_field': 'some data',
1713 'test_string_field': 'some data',
1712 'test_int_field': 100,
1714 'test_int_field': 100,
1713 }
1715 }
1714
1716
1715
1717
1716 @pytest.fixture()
1718 @pytest.fixture()
1717 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1719 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1718 stub_integration_settings):
1720 stub_integration_settings):
1719 integration = IntegrationModel().create(
1721 integration = IntegrationModel().create(
1720 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1722 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1721 name='test repo integration',
1723 name='test repo integration',
1722 repo=repo_stub, repo_group=None, child_repos_only=None)
1724 repo=repo_stub, repo_group=None, child_repos_only=None)
1723
1725
1724 @request.addfinalizer
1726 @request.addfinalizer
1725 def cleanup():
1727 def cleanup():
1726 IntegrationModel().delete(integration)
1728 IntegrationModel().delete(integration)
1727
1729
1728 return integration
1730 return integration
1729
1731
1730
1732
1731 @pytest.fixture()
1733 @pytest.fixture()
1732 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1734 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1733 stub_integration_settings):
1735 stub_integration_settings):
1734 integration = IntegrationModel().create(
1736 integration = IntegrationModel().create(
1735 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1737 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1736 name='test repogroup integration',
1738 name='test repogroup integration',
1737 repo=None, repo_group=test_repo_group, child_repos_only=True)
1739 repo=None, repo_group=test_repo_group, child_repos_only=True)
1738
1740
1739 @request.addfinalizer
1741 @request.addfinalizer
1740 def cleanup():
1742 def cleanup():
1741 IntegrationModel().delete(integration)
1743 IntegrationModel().delete(integration)
1742
1744
1743 return integration
1745 return integration
1744
1746
1745
1747
1746 @pytest.fixture()
1748 @pytest.fixture()
1747 def repogroup_recursive_integration_stub(request, test_repo_group,
1749 def repogroup_recursive_integration_stub(request, test_repo_group,
1748 StubIntegrationType, stub_integration_settings):
1750 StubIntegrationType, stub_integration_settings):
1749 integration = IntegrationModel().create(
1751 integration = IntegrationModel().create(
1750 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1752 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1751 name='test recursive repogroup integration',
1753 name='test recursive repogroup integration',
1752 repo=None, repo_group=test_repo_group, child_repos_only=False)
1754 repo=None, repo_group=test_repo_group, child_repos_only=False)
1753
1755
1754 @request.addfinalizer
1756 @request.addfinalizer
1755 def cleanup():
1757 def cleanup():
1756 IntegrationModel().delete(integration)
1758 IntegrationModel().delete(integration)
1757
1759
1758 return integration
1760 return integration
1759
1761
1760
1762
1761 @pytest.fixture()
1763 @pytest.fixture()
1762 def global_integration_stub(request, StubIntegrationType,
1764 def global_integration_stub(request, StubIntegrationType,
1763 stub_integration_settings):
1765 stub_integration_settings):
1764 integration = IntegrationModel().create(
1766 integration = IntegrationModel().create(
1765 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1767 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1766 name='test global integration',
1768 name='test global integration',
1767 repo=None, repo_group=None, child_repos_only=None)
1769 repo=None, repo_group=None, child_repos_only=None)
1768
1770
1769 @request.addfinalizer
1771 @request.addfinalizer
1770 def cleanup():
1772 def cleanup():
1771 IntegrationModel().delete(integration)
1773 IntegrationModel().delete(integration)
1772
1774
1773 return integration
1775 return integration
1774
1776
1775
1777
1776 @pytest.fixture()
1778 @pytest.fixture()
1777 def root_repos_integration_stub(request, StubIntegrationType,
1779 def root_repos_integration_stub(request, StubIntegrationType,
1778 stub_integration_settings):
1780 stub_integration_settings):
1779 integration = IntegrationModel().create(
1781 integration = IntegrationModel().create(
1780 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1782 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1781 name='test global integration',
1783 name='test global integration',
1782 repo=None, repo_group=None, child_repos_only=True)
1784 repo=None, repo_group=None, child_repos_only=True)
1783
1785
1784 @request.addfinalizer
1786 @request.addfinalizer
1785 def cleanup():
1787 def cleanup():
1786 IntegrationModel().delete(integration)
1788 IntegrationModel().delete(integration)
1787
1789
1788 return integration
1790 return integration
1789
1791
1790
1792
1791 @pytest.fixture()
1793 @pytest.fixture()
1792 def local_dt_to_utc():
1794 def local_dt_to_utc():
1793 def _factory(dt):
1795 def _factory(dt):
1794 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1796 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1795 dateutil.tz.tzutc()).replace(tzinfo=None)
1797 dateutil.tz.tzutc()).replace(tzinfo=None)
1796 return _factory
1798 return _factory
1797
1799
1798
1800
1799 @pytest.fixture()
1801 @pytest.fixture()
1800 def disable_anonymous_user(request, baseapp):
1802 def disable_anonymous_user(request, baseapp):
1801 set_anonymous_access(False)
1803 set_anonymous_access(False)
1802
1804
1803 @request.addfinalizer
1805 @request.addfinalizer
1804 def cleanup():
1806 def cleanup():
1805 set_anonymous_access(True)
1807 set_anonymous_access(True)
1806
1808
1807
1809
1808 @pytest.fixture(scope='module')
1810 @pytest.fixture(scope='module')
1809 def rc_fixture(request):
1811 def rc_fixture(request):
1810 return Fixture()
1812 return Fixture()
1811
1813
1812
1814
1813 @pytest.fixture()
1815 @pytest.fixture()
1814 def repo_groups(request):
1816 def repo_groups(request):
1815 fixture = Fixture()
1817 fixture = Fixture()
1816
1818
1817 session = Session()
1819 session = Session()
1818 zombie_group = fixture.create_repo_group('zombie')
1820 zombie_group = fixture.create_repo_group('zombie')
1819 parent_group = fixture.create_repo_group('parent')
1821 parent_group = fixture.create_repo_group('parent')
1820 child_group = fixture.create_repo_group('parent/child')
1822 child_group = fixture.create_repo_group('parent/child')
1821 groups_in_db = session.query(RepoGroup).all()
1823 groups_in_db = session.query(RepoGroup).all()
1822 assert len(groups_in_db) == 3
1824 assert len(groups_in_db) == 3
1823 assert child_group.group_parent_id == parent_group.group_id
1825 assert child_group.group_parent_id == parent_group.group_id
1824
1826
1825 @request.addfinalizer
1827 @request.addfinalizer
1826 def cleanup():
1828 def cleanup():
1827 fixture.destroy_repo_group(zombie_group)
1829 fixture.destroy_repo_group(zombie_group)
1828 fixture.destroy_repo_group(child_group)
1830 fixture.destroy_repo_group(child_group)
1829 fixture.destroy_repo_group(parent_group)
1831 fixture.destroy_repo_group(parent_group)
1830
1832
1831 return zombie_group, parent_group, child_group
1833 return zombie_group, parent_group, child_group
General Comments 0
You need to be logged in to leave comments. Login now