##// END OF EJS Templates
hooks: deprecate old action_logger
marcink -
r1754:0436d3e6 default
parent child Browse files
Show More
@@ -1,421 +1,413 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of hooks run by RhodeCode Enterprise
24 24 """
25 25
26 26 import os
27 27 import collections
28 28 import logging
29 29
30 30 import rhodecode
31 31 from rhodecode import events
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 from rhodecode.lib.utils import action_logger
35 34 from rhodecode.lib.utils2 import safe_str
36 35 from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError
37 36 from rhodecode.model.db import Repository, User
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 41 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
43 42
44 43
45 44 def is_shadow_repo(extras):
46 45 """
47 46 Returns ``True`` if this is an action executed against a shadow repository.
48 47 """
49 48 return extras['is_shadow_repo']
50 49
51 50
52 51 def _get_scm_size(alias, root_path):
53 52
54 53 if not alias.startswith('.'):
55 54 alias += '.'
56 55
57 56 size_scm, size_root = 0, 0
58 57 for path, unused_dirs, files in os.walk(safe_str(root_path)):
59 58 if path.find(alias) != -1:
60 59 for f in files:
61 60 try:
62 61 size_scm += os.path.getsize(os.path.join(path, f))
63 62 except OSError:
64 63 pass
65 64 else:
66 65 for f in files:
67 66 try:
68 67 size_root += os.path.getsize(os.path.join(path, f))
69 68 except OSError:
70 69 pass
71 70
72 71 size_scm_f = h.format_byte_size_binary(size_scm)
73 72 size_root_f = h.format_byte_size_binary(size_root)
74 73 size_total_f = h.format_byte_size_binary(size_root + size_scm)
75 74
76 75 return size_scm_f, size_root_f, size_total_f
77 76
78 77
79 78 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
80 79 def repo_size(extras):
81 80 """Present size of repository after push."""
82 81 repo = Repository.get_by_repo_name(extras.repository)
83 82 vcs_part = safe_str(u'.%s' % repo.repo_type)
84 83 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
85 84 repo.repo_full_path)
86 85 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
87 86 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
88 87 return HookResponse(0, msg)
89 88
90 89
91 90 def pre_push(extras):
92 91 """
93 92 Hook executed before pushing code.
94 93
95 94 It bans pushing when the repository is locked.
96 95 """
97 96
98 97 usr = User.get_by_username(extras.username)
99 98 output = ''
100 99 if extras.locked_by[0] and usr.user_id != int(extras.locked_by[0]):
101 100 locked_by = User.get(extras.locked_by[0]).username
102 101 reason = extras.locked_by[2]
103 102 # this exception is interpreted in git/hg middlewares and based
104 103 # on that proper return code is server to client
105 104 _http_ret = HTTPLockedRC(
106 105 _locked_by_explanation(extras.repository, locked_by, reason))
107 106 if str(_http_ret.code).startswith('2'):
108 107 # 2xx Codes don't raise exceptions
109 108 output = _http_ret.title
110 109 else:
111 110 raise _http_ret
112 111
113 112 # Propagate to external components. This is done after checking the
114 113 # lock, for consistent behavior.
115 114 if not is_shadow_repo(extras):
116 115 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
117 116 events.trigger(events.RepoPrePushEvent(
118 117 repo_name=extras.repository, extras=extras))
119 118
120 119 return HookResponse(0, output)
121 120
122 121
123 122 def pre_pull(extras):
124 123 """
125 124 Hook executed before pulling the code.
126 125
127 126 It bans pulling when the repository is locked.
128 127 """
129 128
130 129 output = ''
131 130 if extras.locked_by[0]:
132 131 locked_by = User.get(extras.locked_by[0]).username
133 132 reason = extras.locked_by[2]
134 133 # this exception is interpreted in git/hg middlewares and based
135 134 # on that proper return code is server to client
136 135 _http_ret = HTTPLockedRC(
137 136 _locked_by_explanation(extras.repository, locked_by, reason))
138 137 if str(_http_ret.code).startswith('2'):
139 138 # 2xx Codes don't raise exceptions
140 139 output = _http_ret.title
141 140 else:
142 141 raise _http_ret
143 142
144 143 # Propagate to external components. This is done after checking the
145 144 # lock, for consistent behavior.
146 145 if not is_shadow_repo(extras):
147 146 pre_pull_extension(**extras)
148 147 events.trigger(events.RepoPrePullEvent(
149 148 repo_name=extras.repository, extras=extras))
150 149
151 150 return HookResponse(0, output)
152 151
153 152
154 153 def post_pull(extras):
155 154 """Hook executed after client pulls the code."""
156 user = User.get_by_username(extras.username)
157 action = 'pull'
158 action_logger(user, action, extras.repository, extras.ip, commit=True)
159 155
160 156 audit_user = audit_logger.UserWrap(
161 157 username=extras.username,
162 158 ip_addr=extras.ip)
163 159 repo = audit_logger.RepoWrap(repo_name=extras.repository)
164 160 audit_logger.store(
165 161 action='user.pull', action_data={
166 162 'user_agent': extras.user_agent},
167 163 user=audit_user, repo=repo, commit=True)
168 164
169 165 # Propagate to external components.
170 166 if not is_shadow_repo(extras):
171 167 post_pull_extension(**extras)
172 168 events.trigger(events.RepoPullEvent(
173 169 repo_name=extras.repository, extras=extras))
174 170
175 171 output = ''
176 172 # make lock is a tri state False, True, None. We only make lock on True
177 173 if extras.make_lock is True and not is_shadow_repo(extras):
174 user = User.get_by_username(extras.username)
178 175 Repository.lock(Repository.get_by_repo_name(extras.repository),
179 176 user.user_id,
180 177 lock_reason=Repository.LOCK_PULL)
181 178 msg = 'Made lock on repo `%s`' % (extras.repository,)
182 179 output += msg
183 180
184 181 if extras.locked_by[0]:
185 182 locked_by = User.get(extras.locked_by[0]).username
186 183 reason = extras.locked_by[2]
187 184 _http_ret = HTTPLockedRC(
188 185 _locked_by_explanation(extras.repository, locked_by, reason))
189 186 if str(_http_ret.code).startswith('2'):
190 187 # 2xx Codes don't raise exceptions
191 188 output += _http_ret.title
192 189
193 190 return HookResponse(0, output)
194 191
195 192
196 193 def post_push(extras):
197 194 """Hook executed after user pushes to the repository."""
198 action_tmpl = extras.action + ':%s'
199 commit_ids = extras.commit_ids[:29000]
195 commit_ids = extras.commit_ids
200 196
201 action = action_tmpl % ','.join(commit_ids)
202 action_logger(
203 extras.username, action, extras.repository, extras.ip, commit=True)
204
197 # log the push call
205 198 audit_user = audit_logger.UserWrap(
206 username=extras.username,
207 ip_addr=extras.ip)
199 username=extras.username, ip_addr=extras.ip)
208 200 repo = audit_logger.RepoWrap(repo_name=extras.repository)
209 201 audit_logger.store(
210 202 action='user.push', action_data={
211 203 'user_agent': extras.user_agent,
212 204 'commit_ids': commit_ids[:10000]},
213 205 user=audit_user, repo=repo, commit=True)
214 206
215 207 # Propagate to external components.
216 208 if not is_shadow_repo(extras):
217 209 post_push_extension(
218 210 repo_store_path=Repository.base_path(),
219 211 pushed_revs=commit_ids,
220 212 **extras)
221 213 events.trigger(events.RepoPushEvent(
222 214 repo_name=extras.repository,
223 215 pushed_commit_ids=commit_ids,
224 216 extras=extras))
225 217
226 218 output = ''
227 219 # make lock is a tri state False, True, None. We only release lock on False
228 220 if extras.make_lock is False and not is_shadow_repo(extras):
229 221 Repository.unlock(Repository.get_by_repo_name(extras.repository))
230 222 msg = 'Released lock on repo `%s`\n' % extras.repository
231 223 output += msg
232 224
233 225 if extras.locked_by[0]:
234 226 locked_by = User.get(extras.locked_by[0]).username
235 227 reason = extras.locked_by[2]
236 228 _http_ret = HTTPLockedRC(
237 229 _locked_by_explanation(extras.repository, locked_by, reason))
238 230 # TODO: johbo: if not?
239 231 if str(_http_ret.code).startswith('2'):
240 232 # 2xx Codes don't raise exceptions
241 233 output += _http_ret.title
242 234
243 235 output += 'RhodeCode: push completed\n'
244 236
245 237 return HookResponse(0, output)
246 238
247 239
248 240 def _locked_by_explanation(repo_name, user_name, reason):
249 241 message = (
250 242 'Repository `%s` locked by user `%s`. Reason:`%s`'
251 243 % (repo_name, user_name, reason))
252 244 return message
253 245
254 246
255 247 def check_allowed_create_user(user_dict, created_by, **kwargs):
256 248 # pre create hooks
257 249 if pre_create_user.is_active():
258 250 allowed, reason = pre_create_user(created_by=created_by, **user_dict)
259 251 if not allowed:
260 252 raise UserCreationError(reason)
261 253
262 254
263 255 class ExtensionCallback(object):
264 256 """
265 257 Forwards a given call to rcextensions, sanitizes keyword arguments.
266 258
267 259 Does check if there is an extension active for that hook. If it is
268 260 there, it will forward all `kwargs_keys` keyword arguments to the
269 261 extension callback.
270 262 """
271 263
272 264 def __init__(self, hook_name, kwargs_keys):
273 265 self._hook_name = hook_name
274 266 self._kwargs_keys = set(kwargs_keys)
275 267
276 268 def __call__(self, *args, **kwargs):
277 269 log.debug('Calling extension callback for %s', self._hook_name)
278 270
279 271 kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys)
280 272 # backward compat for removed api_key for old hooks. THis was it works
281 273 # with older rcextensions that require api_key present
282 274 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
283 275 kwargs_to_pass['api_key'] = '_DEPRECATED_'
284 276
285 277 callback = self._get_callback()
286 278 if callback:
287 279 return callback(**kwargs_to_pass)
288 280 else:
289 281 log.debug('extensions callback not found skipping...')
290 282
291 283 def is_active(self):
292 284 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
293 285
294 286 def _get_callback(self):
295 287 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
296 288
297 289
298 290 pre_pull_extension = ExtensionCallback(
299 291 hook_name='PRE_PULL_HOOK',
300 292 kwargs_keys=(
301 293 'server_url', 'config', 'scm', 'username', 'ip', 'action',
302 294 'repository'))
303 295
304 296
305 297 post_pull_extension = ExtensionCallback(
306 298 hook_name='PULL_HOOK',
307 299 kwargs_keys=(
308 300 'server_url', 'config', 'scm', 'username', 'ip', 'action',
309 301 'repository'))
310 302
311 303
312 304 pre_push_extension = ExtensionCallback(
313 305 hook_name='PRE_PUSH_HOOK',
314 306 kwargs_keys=(
315 307 'server_url', 'config', 'scm', 'username', 'ip', 'action',
316 308 'repository', 'repo_store_path', 'commit_ids'))
317 309
318 310
319 311 post_push_extension = ExtensionCallback(
320 312 hook_name='PUSH_HOOK',
321 313 kwargs_keys=(
322 314 'server_url', 'config', 'scm', 'username', 'ip', 'action',
323 315 'repository', 'repo_store_path', 'pushed_revs'))
324 316
325 317
326 318 pre_create_user = ExtensionCallback(
327 319 hook_name='PRE_CREATE_USER_HOOK',
328 320 kwargs_keys=(
329 321 'username', 'password', 'email', 'firstname', 'lastname', 'active',
330 322 'admin', 'created_by'))
331 323
332 324
333 325 log_create_pull_request = ExtensionCallback(
334 326 hook_name='CREATE_PULL_REQUEST',
335 327 kwargs_keys=(
336 328 'server_url', 'config', 'scm', 'username', 'ip', 'action',
337 329 'repository', 'pull_request_id', 'url', 'title', 'description',
338 330 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
339 331 'mergeable', 'source', 'target', 'author', 'reviewers'))
340 332
341 333
342 334 log_merge_pull_request = ExtensionCallback(
343 335 hook_name='MERGE_PULL_REQUEST',
344 336 kwargs_keys=(
345 337 'server_url', 'config', 'scm', 'username', 'ip', 'action',
346 338 'repository', 'pull_request_id', 'url', 'title', 'description',
347 339 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
348 340 'mergeable', 'source', 'target', 'author', 'reviewers'))
349 341
350 342
351 343 log_close_pull_request = ExtensionCallback(
352 344 hook_name='CLOSE_PULL_REQUEST',
353 345 kwargs_keys=(
354 346 'server_url', 'config', 'scm', 'username', 'ip', 'action',
355 347 'repository', 'pull_request_id', 'url', 'title', 'description',
356 348 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
357 349 'mergeable', 'source', 'target', 'author', 'reviewers'))
358 350
359 351
360 352 log_review_pull_request = ExtensionCallback(
361 353 hook_name='REVIEW_PULL_REQUEST',
362 354 kwargs_keys=(
363 355 'server_url', 'config', 'scm', 'username', 'ip', 'action',
364 356 'repository', 'pull_request_id', 'url', 'title', 'description',
365 357 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
366 358 'mergeable', 'source', 'target', 'author', 'reviewers'))
367 359
368 360
369 361 log_update_pull_request = ExtensionCallback(
370 362 hook_name='UPDATE_PULL_REQUEST',
371 363 kwargs_keys=(
372 364 'server_url', 'config', 'scm', 'username', 'ip', 'action',
373 365 'repository', 'pull_request_id', 'url', 'title', 'description',
374 366 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
375 367 'mergeable', 'source', 'target', 'author', 'reviewers'))
376 368
377 369
378 370 log_create_user = ExtensionCallback(
379 371 hook_name='CREATE_USER_HOOK',
380 372 kwargs_keys=(
381 373 'username', 'full_name_or_username', 'full_contact', 'user_id',
382 374 'name', 'firstname', 'short_contact', 'admin', 'lastname',
383 375 'ip_addresses', 'extern_type', 'extern_name',
384 376 'email', 'api_keys', 'last_login',
385 377 'full_name', 'active', 'password', 'emails',
386 378 'inherit_default_permissions', 'created_by', 'created_on'))
387 379
388 380
389 381 log_delete_user = ExtensionCallback(
390 382 hook_name='DELETE_USER_HOOK',
391 383 kwargs_keys=(
392 384 'username', 'full_name_or_username', 'full_contact', 'user_id',
393 385 'name', 'firstname', 'short_contact', 'admin', 'lastname',
394 386 'ip_addresses',
395 387 'email', 'last_login',
396 388 'full_name', 'active', 'password', 'emails',
397 389 'inherit_default_permissions', 'deleted_by'))
398 390
399 391
400 392 log_create_repository = ExtensionCallback(
401 393 hook_name='CREATE_REPO_HOOK',
402 394 kwargs_keys=(
403 395 'repo_name', 'repo_type', 'description', 'private', 'created_on',
404 396 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
405 397 'clone_uri', 'fork_id', 'group_id', 'created_by'))
406 398
407 399
408 400 log_delete_repository = ExtensionCallback(
409 401 hook_name='DELETE_REPO_HOOK',
410 402 kwargs_keys=(
411 403 'repo_name', 'repo_type', 'description', 'private', 'created_on',
412 404 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
413 405 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
414 406
415 407
416 408 log_create_repository_group = ExtensionCallback(
417 409 hook_name='CREATE_REPO_GROUP_HOOK',
418 410 kwargs_keys=(
419 411 'group_name', 'group_parent_id', 'group_description',
420 412 'group_id', 'user_id', 'created_by', 'created_on',
421 413 'enable_locking'))
@@ -1,1097 +1,1103 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23 from webob.exc import HTTPNotFound
24 24
25 25 import rhodecode
26 26 from rhodecode.lib.vcs.nodes import FileNode
27 27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 28 from rhodecode.model.db import (
29 29 PullRequest, ChangesetStatus, UserLog, Notification)
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.model.repo import RepoModel
34 34 from rhodecode.tests import (
35 35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 36 from rhodecode.tests.utils import AssertResponse
37 37
38 38
39 39 @pytest.mark.usefixtures('app', 'autologin_user')
40 40 @pytest.mark.backends("git", "hg")
41 41 class TestPullrequestsController:
42 42
43 43 def test_index(self, backend):
44 44 self.app.get(url(
45 45 controller='pullrequests', action='index',
46 46 repo_name=backend.repo_name))
47 47
48 48 def test_option_menu_create_pull_request_exists(self, backend):
49 49 repo_name = backend.repo_name
50 50 response = self.app.get(url('summary_home', repo_name=repo_name))
51 51
52 52 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
53 53 'pullrequest', repo_name=repo_name)
54 54 response.mustcontain(create_pr_link)
55 55
56 56 def test_global_redirect_of_pr(self, backend, pr_util):
57 57 pull_request = pr_util.create_pull_request()
58 58
59 59 response = self.app.get(
60 60 url('pull_requests_global',
61 61 pull_request_id=pull_request.pull_request_id))
62 62
63 63 repo_name = pull_request.target_repo.repo_name
64 64 redirect_url = url('pullrequest_show', repo_name=repo_name,
65 65 pull_request_id=pull_request.pull_request_id)
66 66 assert response.status == '302 Found'
67 67 assert redirect_url in response.location
68 68
69 69 def test_create_pr_form_with_raw_commit_id(self, backend):
70 70 repo = backend.repo
71 71
72 72 self.app.get(
73 73 url(controller='pullrequests', action='index',
74 74 repo_name=repo.repo_name,
75 75 commit=repo.get_commit().raw_id),
76 76 status=200)
77 77
78 78 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
79 79 def test_show(self, pr_util, pr_merge_enabled):
80 80 pull_request = pr_util.create_pull_request(
81 81 mergeable=pr_merge_enabled, enable_notifications=False)
82 82
83 83 response = self.app.get(url(
84 84 controller='pullrequests', action='show',
85 85 repo_name=pull_request.target_repo.scm_instance().name,
86 86 pull_request_id=str(pull_request.pull_request_id)))
87 87
88 88 for commit_id in pull_request.revisions:
89 89 response.mustcontain(commit_id)
90 90
91 91 assert pull_request.target_ref_parts.type in response
92 92 assert pull_request.target_ref_parts.name in response
93 93 target_clone_url = pull_request.target_repo.clone_url()
94 94 assert target_clone_url in response
95 95
96 96 assert 'class="pull-request-merge"' in response
97 97 assert (
98 98 'Server-side pull request merging is disabled.'
99 99 in response) != pr_merge_enabled
100 100
101 101 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
102 102 from rhodecode.tests.functional.test_login import login_url, logut_url
103 103 # Logout
104 104 response = self.app.post(
105 105 logut_url,
106 106 params={'csrf_token': csrf_token})
107 107 # Login as regular user
108 108 response = self.app.post(login_url,
109 109 {'username': TEST_USER_REGULAR_LOGIN,
110 110 'password': 'test12'})
111 111
112 112 pull_request = pr_util.create_pull_request(
113 113 author=TEST_USER_REGULAR_LOGIN)
114 114
115 115 response = self.app.get(url(
116 116 controller='pullrequests', action='show',
117 117 repo_name=pull_request.target_repo.scm_instance().name,
118 118 pull_request_id=str(pull_request.pull_request_id)))
119 119
120 120 response.mustcontain('Server-side pull request merging is disabled.')
121 121
122 122 assert_response = response.assert_response()
123 123 # for regular user without a merge permissions, we don't see it
124 124 assert_response.no_element_exists('#close-pull-request-action')
125 125
126 126 user_util.grant_user_permission_to_repo(
127 127 pull_request.target_repo,
128 128 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
129 129 'repository.write')
130 130 response = self.app.get(url(
131 131 controller='pullrequests', action='show',
132 132 repo_name=pull_request.target_repo.scm_instance().name,
133 133 pull_request_id=str(pull_request.pull_request_id)))
134 134
135 135 response.mustcontain('Server-side pull request merging is disabled.')
136 136
137 137 assert_response = response.assert_response()
138 138 # now regular user has a merge permissions, we have CLOSE button
139 139 assert_response.one_element_exists('#close-pull-request-action')
140 140
141 141 def test_show_invalid_commit_id(self, pr_util):
142 142 # Simulating invalid revisions which will cause a lookup error
143 143 pull_request = pr_util.create_pull_request()
144 144 pull_request.revisions = ['invalid']
145 145 Session().add(pull_request)
146 146 Session().commit()
147 147
148 148 response = self.app.get(url(
149 149 controller='pullrequests', action='show',
150 150 repo_name=pull_request.target_repo.scm_instance().name,
151 151 pull_request_id=str(pull_request.pull_request_id)))
152 152
153 153 for commit_id in pull_request.revisions:
154 154 response.mustcontain(commit_id)
155 155
156 156 def test_show_invalid_source_reference(self, pr_util):
157 157 pull_request = pr_util.create_pull_request()
158 158 pull_request.source_ref = 'branch:b:invalid'
159 159 Session().add(pull_request)
160 160 Session().commit()
161 161
162 162 self.app.get(url(
163 163 controller='pullrequests', action='show',
164 164 repo_name=pull_request.target_repo.scm_instance().name,
165 165 pull_request_id=str(pull_request.pull_request_id)))
166 166
167 167 def test_edit_title_description(self, pr_util, csrf_token):
168 168 pull_request = pr_util.create_pull_request()
169 169 pull_request_id = pull_request.pull_request_id
170 170
171 171 response = self.app.post(
172 172 url(controller='pullrequests', action='update',
173 173 repo_name=pull_request.target_repo.repo_name,
174 174 pull_request_id=str(pull_request_id)),
175 175 params={
176 176 'edit_pull_request': 'true',
177 177 '_method': 'put',
178 178 'title': 'New title',
179 179 'description': 'New description',
180 180 'csrf_token': csrf_token})
181 181
182 182 assert_session_flash(
183 183 response, u'Pull request title & description updated.',
184 184 category='success')
185 185
186 186 pull_request = PullRequest.get(pull_request_id)
187 187 assert pull_request.title == 'New title'
188 188 assert pull_request.description == 'New description'
189 189
190 190 def test_edit_title_description_closed(self, pr_util, csrf_token):
191 191 pull_request = pr_util.create_pull_request()
192 192 pull_request_id = pull_request.pull_request_id
193 193 pr_util.close()
194 194
195 195 response = self.app.post(
196 196 url(controller='pullrequests', action='update',
197 197 repo_name=pull_request.target_repo.repo_name,
198 198 pull_request_id=str(pull_request_id)),
199 199 params={
200 200 'edit_pull_request': 'true',
201 201 '_method': 'put',
202 202 'title': 'New title',
203 203 'description': 'New description',
204 204 'csrf_token': csrf_token})
205 205
206 206 assert_session_flash(
207 207 response, u'Cannot update closed pull requests.',
208 208 category='error')
209 209
210 210 def test_update_invalid_source_reference(self, pr_util, csrf_token):
211 211 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
212 212
213 213 pull_request = pr_util.create_pull_request()
214 214 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
215 215 Session().add(pull_request)
216 216 Session().commit()
217 217
218 218 pull_request_id = pull_request.pull_request_id
219 219
220 220 response = self.app.post(
221 221 url(controller='pullrequests', action='update',
222 222 repo_name=pull_request.target_repo.repo_name,
223 223 pull_request_id=str(pull_request_id)),
224 224 params={'update_commits': 'true', '_method': 'put',
225 225 'csrf_token': csrf_token})
226 226
227 227 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
228 228 UpdateFailureReason.MISSING_SOURCE_REF]
229 229 assert_session_flash(response, expected_msg, category='error')
230 230
231 231 def test_missing_target_reference(self, pr_util, csrf_token):
232 232 from rhodecode.lib.vcs.backends.base import MergeFailureReason
233 233 pull_request = pr_util.create_pull_request(
234 234 approved=True, mergeable=True)
235 235 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
236 236 Session().add(pull_request)
237 237 Session().commit()
238 238
239 239 pull_request_id = pull_request.pull_request_id
240 240 pull_request_url = url(
241 241 controller='pullrequests', action='show',
242 242 repo_name=pull_request.target_repo.repo_name,
243 243 pull_request_id=str(pull_request_id))
244 244
245 245 response = self.app.get(pull_request_url)
246 246
247 247 assertr = AssertResponse(response)
248 248 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
249 249 MergeFailureReason.MISSING_TARGET_REF]
250 250 assertr.element_contains(
251 251 'span[data-role="merge-message"]', str(expected_msg))
252 252
253 253 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
254 254 pull_request = pr_util.create_pull_request(approved=True)
255 255 pull_request_id = pull_request.pull_request_id
256 256 author = pull_request.user_id
257 257 repo = pull_request.target_repo.repo_id
258 258
259 259 self.app.post(
260 260 url(controller='pullrequests',
261 261 action='comment',
262 262 repo_name=pull_request.target_repo.scm_instance().name,
263 263 pull_request_id=str(pull_request_id)),
264 264 params={
265 265 'changeset_status': ChangesetStatus.STATUS_APPROVED,
266 266 'close_pull_request': '1',
267 267 'text': 'Closing a PR',
268 268 'csrf_token': csrf_token},
269 269 status=302)
270 270
271 271 action = 'user_closed_pull_request:%d' % pull_request_id
272 272 journal = UserLog.query()\
273 273 .filter(UserLog.user_id == author)\
274 274 .filter(UserLog.repository_id == repo)\
275 275 .filter(UserLog.action == action)\
276 276 .all()
277 277 assert len(journal) == 1
278 278
279 279 pull_request = PullRequest.get(pull_request_id)
280 280 assert pull_request.is_closed()
281 281
282 282 # check only the latest status, not the review status
283 283 status = ChangesetStatusModel().get_status(
284 284 pull_request.source_repo, pull_request=pull_request)
285 285 assert status == ChangesetStatus.STATUS_APPROVED
286 286
287 287 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
288 288 pull_request = pr_util.create_pull_request()
289 289 pull_request_id = pull_request.pull_request_id
290 290 response = self.app.post(
291 291 url(controller='pullrequests',
292 292 action='update',
293 293 repo_name=pull_request.target_repo.scm_instance().name,
294 294 pull_request_id=str(pull_request.pull_request_id)),
295 295 params={'close_pull_request': 'true', '_method': 'put',
296 296 'csrf_token': csrf_token})
297 297
298 298 pull_request = PullRequest.get(pull_request_id)
299 299
300 300 assert response.json is True
301 301 assert pull_request.is_closed()
302 302
303 303 # check only the latest status, not the review status
304 304 status = ChangesetStatusModel().get_status(
305 305 pull_request.source_repo, pull_request=pull_request)
306 306 assert status == ChangesetStatus.STATUS_REJECTED
307 307
308 308 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
309 309 pull_request = pr_util.create_pull_request()
310 310 pull_request_id = pull_request.pull_request_id
311 311 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
312 312 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
313 313 author = pull_request.user_id
314 314 repo = pull_request.target_repo.repo_id
315 315 self.app.post(
316 316 url(controller='pullrequests',
317 317 action='comment',
318 318 repo_name=pull_request.target_repo.scm_instance().name,
319 319 pull_request_id=str(pull_request_id)),
320 320 params={
321 321 'changeset_status': 'rejected',
322 322 'close_pull_request': '1',
323 323 'csrf_token': csrf_token},
324 324 status=302)
325 325
326 326 pull_request = PullRequest.get(pull_request_id)
327 327
328 328 action = 'user_closed_pull_request:%d' % pull_request_id
329 329 journal = UserLog.query().filter(
330 330 UserLog.user_id == author,
331 331 UserLog.repository_id == repo,
332 332 UserLog.action == action).all()
333 333 assert len(journal) == 1
334 334
335 335 # check only the latest status, not the review status
336 336 status = ChangesetStatusModel().get_status(
337 337 pull_request.source_repo, pull_request=pull_request)
338 338 assert status == ChangesetStatus.STATUS_REJECTED
339 339
340 340 def test_create_pull_request(self, backend, csrf_token):
341 341 commits = [
342 342 {'message': 'ancestor'},
343 343 {'message': 'change'},
344 344 {'message': 'change2'},
345 345 ]
346 346 commit_ids = backend.create_master_repo(commits)
347 347 target = backend.create_repo(heads=['ancestor'])
348 348 source = backend.create_repo(heads=['change2'])
349 349
350 350 response = self.app.post(
351 351 url(
352 352 controller='pullrequests',
353 353 action='create',
354 354 repo_name=source.repo_name
355 355 ),
356 356 [
357 357 ('source_repo', source.repo_name),
358 358 ('source_ref', 'branch:default:' + commit_ids['change2']),
359 359 ('target_repo', target.repo_name),
360 360 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
361 361 ('pullrequest_desc', 'Description'),
362 362 ('pullrequest_title', 'Title'),
363 363 ('__start__', 'review_members:sequence'),
364 364 ('__start__', 'reviewer:mapping'),
365 365 ('user_id', '1'),
366 366 ('__start__', 'reasons:sequence'),
367 367 ('reason', 'Some reason'),
368 368 ('__end__', 'reasons:sequence'),
369 369 ('__end__', 'reviewer:mapping'),
370 370 ('__end__', 'review_members:sequence'),
371 371 ('__start__', 'revisions:sequence'),
372 372 ('revisions', commit_ids['change']),
373 373 ('revisions', commit_ids['change2']),
374 374 ('__end__', 'revisions:sequence'),
375 375 ('user', ''),
376 376 ('csrf_token', csrf_token),
377 377 ],
378 378 status=302)
379 379
380 380 location = response.headers['Location']
381 381 pull_request_id = int(location.rsplit('/', 1)[1])
382 382 pull_request = PullRequest.get(pull_request_id)
383 383
384 384 # check that we have now both revisions
385 385 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
386 386 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
387 387 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
388 388 assert pull_request.target_ref == expected_target_ref
389 389
390 390 def test_reviewer_notifications(self, backend, csrf_token):
391 391 # We have to use the app.post for this test so it will create the
392 392 # notifications properly with the new PR
393 393 commits = [
394 394 {'message': 'ancestor',
395 395 'added': [FileNode('file_A', content='content_of_ancestor')]},
396 396 {'message': 'change',
397 397 'added': [FileNode('file_a', content='content_of_change')]},
398 398 {'message': 'change-child'},
399 399 {'message': 'ancestor-child', 'parents': ['ancestor'],
400 400 'added': [
401 401 FileNode('file_B', content='content_of_ancestor_child')]},
402 402 {'message': 'ancestor-child-2'},
403 403 ]
404 404 commit_ids = backend.create_master_repo(commits)
405 405 target = backend.create_repo(heads=['ancestor-child'])
406 406 source = backend.create_repo(heads=['change'])
407 407
408 408 response = self.app.post(
409 409 url(
410 410 controller='pullrequests',
411 411 action='create',
412 412 repo_name=source.repo_name
413 413 ),
414 414 [
415 415 ('source_repo', source.repo_name),
416 416 ('source_ref', 'branch:default:' + commit_ids['change']),
417 417 ('target_repo', target.repo_name),
418 418 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
419 419 ('pullrequest_desc', 'Description'),
420 420 ('pullrequest_title', 'Title'),
421 421 ('__start__', 'review_members:sequence'),
422 422 ('__start__', 'reviewer:mapping'),
423 423 ('user_id', '2'),
424 424 ('__start__', 'reasons:sequence'),
425 425 ('reason', 'Some reason'),
426 426 ('__end__', 'reasons:sequence'),
427 427 ('__end__', 'reviewer:mapping'),
428 428 ('__end__', 'review_members:sequence'),
429 429 ('__start__', 'revisions:sequence'),
430 430 ('revisions', commit_ids['change']),
431 431 ('__end__', 'revisions:sequence'),
432 432 ('user', ''),
433 433 ('csrf_token', csrf_token),
434 434 ],
435 435 status=302)
436 436
437 437 location = response.headers['Location']
438 438 pull_request_id = int(location.rsplit('/', 1)[1])
439 439 pull_request = PullRequest.get(pull_request_id)
440 440
441 441 # Check that a notification was made
442 442 notifications = Notification.query()\
443 443 .filter(Notification.created_by == pull_request.author.user_id,
444 444 Notification.type_ == Notification.TYPE_PULL_REQUEST,
445 445 Notification.subject.contains("wants you to review "
446 446 "pull request #%d"
447 447 % pull_request_id))
448 448 assert len(notifications.all()) == 1
449 449
450 450 # Change reviewers and check that a notification was made
451 451 PullRequestModel().update_reviewers(
452 452 pull_request.pull_request_id, [(1, [])])
453 453 assert len(notifications.all()) == 2
454 454
455 455 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
456 456 csrf_token):
457 457 commits = [
458 458 {'message': 'ancestor',
459 459 'added': [FileNode('file_A', content='content_of_ancestor')]},
460 460 {'message': 'change',
461 461 'added': [FileNode('file_a', content='content_of_change')]},
462 462 {'message': 'change-child'},
463 463 {'message': 'ancestor-child', 'parents': ['ancestor'],
464 464 'added': [
465 465 FileNode('file_B', content='content_of_ancestor_child')]},
466 466 {'message': 'ancestor-child-2'},
467 467 ]
468 468 commit_ids = backend.create_master_repo(commits)
469 469 target = backend.create_repo(heads=['ancestor-child'])
470 470 source = backend.create_repo(heads=['change'])
471 471
472 472 response = self.app.post(
473 473 url(
474 474 controller='pullrequests',
475 475 action='create',
476 476 repo_name=source.repo_name
477 477 ),
478 478 [
479 479 ('source_repo', source.repo_name),
480 480 ('source_ref', 'branch:default:' + commit_ids['change']),
481 481 ('target_repo', target.repo_name),
482 482 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
483 483 ('pullrequest_desc', 'Description'),
484 484 ('pullrequest_title', 'Title'),
485 485 ('__start__', 'review_members:sequence'),
486 486 ('__start__', 'reviewer:mapping'),
487 487 ('user_id', '1'),
488 488 ('__start__', 'reasons:sequence'),
489 489 ('reason', 'Some reason'),
490 490 ('__end__', 'reasons:sequence'),
491 491 ('__end__', 'reviewer:mapping'),
492 492 ('__end__', 'review_members:sequence'),
493 493 ('__start__', 'revisions:sequence'),
494 494 ('revisions', commit_ids['change']),
495 495 ('__end__', 'revisions:sequence'),
496 496 ('user', ''),
497 497 ('csrf_token', csrf_token),
498 498 ],
499 499 status=302)
500 500
501 501 location = response.headers['Location']
502 502 pull_request_id = int(location.rsplit('/', 1)[1])
503 503 pull_request = PullRequest.get(pull_request_id)
504 504
505 505 # target_ref has to point to the ancestor's commit_id in order to
506 506 # show the correct diff
507 507 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
508 508 assert pull_request.target_ref == expected_target_ref
509 509
510 510 # Check generated diff contents
511 511 response = response.follow()
512 512 assert 'content_of_ancestor' not in response.body
513 513 assert 'content_of_ancestor-child' not in response.body
514 514 assert 'content_of_change' in response.body
515 515
516 516 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
517 517 # Clear any previous calls to rcextensions
518 518 rhodecode.EXTENSIONS.calls.clear()
519 519
520 520 pull_request = pr_util.create_pull_request(
521 521 approved=True, mergeable=True)
522 522 pull_request_id = pull_request.pull_request_id
523 523 repo_name = pull_request.target_repo.scm_instance().name,
524 524
525 525 response = self.app.post(
526 526 url(controller='pullrequests',
527 527 action='merge',
528 528 repo_name=str(repo_name[0]),
529 529 pull_request_id=str(pull_request_id)),
530 530 params={'csrf_token': csrf_token}).follow()
531 531
532 532 pull_request = PullRequest.get(pull_request_id)
533 533
534 534 assert response.status_int == 200
535 535 assert pull_request.is_closed()
536 536 assert_pull_request_status(
537 537 pull_request, ChangesetStatus.STATUS_APPROVED)
538 538
539 539 # Check the relevant log entries were added
540 540 user_logs = UserLog.query() \
541 541 .filter(UserLog.version == UserLog.VERSION_1) \
542 .order_by('-user_log_id').limit(4)
542 .order_by('-user_log_id').limit(3)
543 543 actions = [log.action for log in user_logs]
544 544 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
545 545 expected_actions = [
546 546 u'user_closed_pull_request:%d' % pull_request_id,
547 547 u'user_merged_pull_request:%d' % pull_request_id,
548 548 # The action below reflect that the post push actions were executed
549 549 u'user_commented_pull_request:%d' % pull_request_id,
550 u'push:%s' % ','.join(pr_commit_ids),
551 550 ]
552 551 assert actions == expected_actions
553 552
553 user_logs = UserLog.query() \
554 .filter(UserLog.version == UserLog.VERSION_2) \
555 .order_by('-user_log_id').limit(1)
556 actions = [log.action for log in user_logs]
557 assert actions == ['user.push']
558 assert user_logs[0].action_data['commit_ids'] == pr_commit_ids
559
554 560 # Check post_push rcextension was really executed
555 561 push_calls = rhodecode.EXTENSIONS.calls['post_push']
556 562 assert len(push_calls) == 1
557 563 unused_last_call_args, last_call_kwargs = push_calls[0]
558 564 assert last_call_kwargs['action'] == 'push'
559 565 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
560 566
561 567 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
562 568 pull_request = pr_util.create_pull_request(mergeable=False)
563 569 pull_request_id = pull_request.pull_request_id
564 570 pull_request = PullRequest.get(pull_request_id)
565 571
566 572 response = self.app.post(
567 573 url(controller='pullrequests',
568 574 action='merge',
569 575 repo_name=pull_request.target_repo.scm_instance().name,
570 576 pull_request_id=str(pull_request.pull_request_id)),
571 577 params={'csrf_token': csrf_token}).follow()
572 578
573 579 assert response.status_int == 200
574 580 response.mustcontain(
575 581 'Merge is not currently possible because of below failed checks.')
576 582 response.mustcontain('Server-side pull request merging is disabled.')
577 583
578 584 @pytest.mark.skip_backends('svn')
579 585 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
580 586 pull_request = pr_util.create_pull_request(mergeable=True)
581 587 pull_request_id = pull_request.pull_request_id
582 588 repo_name = pull_request.target_repo.scm_instance().name,
583 589
584 590 response = self.app.post(
585 591 url(controller='pullrequests',
586 592 action='merge',
587 593 repo_name=str(repo_name[0]),
588 594 pull_request_id=str(pull_request_id)),
589 595 params={'csrf_token': csrf_token}).follow()
590 596
591 597 assert response.status_int == 200
592 598
593 599 response.mustcontain(
594 600 'Merge is not currently possible because of below failed checks.')
595 601 response.mustcontain('Pull request reviewer approval is pending.')
596 602
597 603 def test_update_source_revision(self, backend, csrf_token):
598 604 commits = [
599 605 {'message': 'ancestor'},
600 606 {'message': 'change'},
601 607 {'message': 'change-2'},
602 608 ]
603 609 commit_ids = backend.create_master_repo(commits)
604 610 target = backend.create_repo(heads=['ancestor'])
605 611 source = backend.create_repo(heads=['change'])
606 612
607 613 # create pr from a in source to A in target
608 614 pull_request = PullRequest()
609 615 pull_request.source_repo = source
610 616 # TODO: johbo: Make sure that we write the source ref this way!
611 617 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
612 618 branch=backend.default_branch_name, commit_id=commit_ids['change'])
613 619 pull_request.target_repo = target
614 620
615 621 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
616 622 branch=backend.default_branch_name,
617 623 commit_id=commit_ids['ancestor'])
618 624 pull_request.revisions = [commit_ids['change']]
619 625 pull_request.title = u"Test"
620 626 pull_request.description = u"Description"
621 627 pull_request.author = UserModel().get_by_username(
622 628 TEST_USER_ADMIN_LOGIN)
623 629 Session().add(pull_request)
624 630 Session().commit()
625 631 pull_request_id = pull_request.pull_request_id
626 632
627 633 # source has ancestor - change - change-2
628 634 backend.pull_heads(source, heads=['change-2'])
629 635
630 636 # update PR
631 637 self.app.post(
632 638 url(controller='pullrequests', action='update',
633 639 repo_name=target.repo_name,
634 640 pull_request_id=str(pull_request_id)),
635 641 params={'update_commits': 'true', '_method': 'put',
636 642 'csrf_token': csrf_token})
637 643
638 644 # check that we have now both revisions
639 645 pull_request = PullRequest.get(pull_request_id)
640 646 assert pull_request.revisions == [
641 647 commit_ids['change-2'], commit_ids['change']]
642 648
643 649 # TODO: johbo: this should be a test on its own
644 650 response = self.app.get(url(
645 651 controller='pullrequests', action='index',
646 652 repo_name=target.repo_name))
647 653 assert response.status_int == 200
648 654 assert 'Pull request updated to' in response.body
649 655 assert 'with 1 added, 0 removed commits.' in response.body
650 656
651 657 def test_update_target_revision(self, backend, csrf_token):
652 658 commits = [
653 659 {'message': 'ancestor'},
654 660 {'message': 'change'},
655 661 {'message': 'ancestor-new', 'parents': ['ancestor']},
656 662 {'message': 'change-rebased'},
657 663 ]
658 664 commit_ids = backend.create_master_repo(commits)
659 665 target = backend.create_repo(heads=['ancestor'])
660 666 source = backend.create_repo(heads=['change'])
661 667
662 668 # create pr from a in source to A in target
663 669 pull_request = PullRequest()
664 670 pull_request.source_repo = source
665 671 # TODO: johbo: Make sure that we write the source ref this way!
666 672 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
667 673 branch=backend.default_branch_name, commit_id=commit_ids['change'])
668 674 pull_request.target_repo = target
669 675 # TODO: johbo: Target ref should be branch based, since tip can jump
670 676 # from branch to branch
671 677 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
672 678 branch=backend.default_branch_name,
673 679 commit_id=commit_ids['ancestor'])
674 680 pull_request.revisions = [commit_ids['change']]
675 681 pull_request.title = u"Test"
676 682 pull_request.description = u"Description"
677 683 pull_request.author = UserModel().get_by_username(
678 684 TEST_USER_ADMIN_LOGIN)
679 685 Session().add(pull_request)
680 686 Session().commit()
681 687 pull_request_id = pull_request.pull_request_id
682 688
683 689 # target has ancestor - ancestor-new
684 690 # source has ancestor - ancestor-new - change-rebased
685 691 backend.pull_heads(target, heads=['ancestor-new'])
686 692 backend.pull_heads(source, heads=['change-rebased'])
687 693
688 694 # update PR
689 695 self.app.post(
690 696 url(controller='pullrequests', action='update',
691 697 repo_name=target.repo_name,
692 698 pull_request_id=str(pull_request_id)),
693 699 params={'update_commits': 'true', '_method': 'put',
694 700 'csrf_token': csrf_token},
695 701 status=200)
696 702
697 703 # check that we have now both revisions
698 704 pull_request = PullRequest.get(pull_request_id)
699 705 assert pull_request.revisions == [commit_ids['change-rebased']]
700 706 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
701 707 branch=backend.default_branch_name,
702 708 commit_id=commit_ids['ancestor-new'])
703 709
704 710 # TODO: johbo: This should be a test on its own
705 711 response = self.app.get(url(
706 712 controller='pullrequests', action='index',
707 713 repo_name=target.repo_name))
708 714 assert response.status_int == 200
709 715 assert 'Pull request updated to' in response.body
710 716 assert 'with 1 added, 1 removed commits.' in response.body
711 717
712 718 def test_update_of_ancestor_reference(self, backend, csrf_token):
713 719 commits = [
714 720 {'message': 'ancestor'},
715 721 {'message': 'change'},
716 722 {'message': 'change-2'},
717 723 {'message': 'ancestor-new', 'parents': ['ancestor']},
718 724 {'message': 'change-rebased'},
719 725 ]
720 726 commit_ids = backend.create_master_repo(commits)
721 727 target = backend.create_repo(heads=['ancestor'])
722 728 source = backend.create_repo(heads=['change'])
723 729
724 730 # create pr from a in source to A in target
725 731 pull_request = PullRequest()
726 732 pull_request.source_repo = source
727 733 # TODO: johbo: Make sure that we write the source ref this way!
728 734 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
729 735 branch=backend.default_branch_name,
730 736 commit_id=commit_ids['change'])
731 737 pull_request.target_repo = target
732 738 # TODO: johbo: Target ref should be branch based, since tip can jump
733 739 # from branch to branch
734 740 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
735 741 branch=backend.default_branch_name,
736 742 commit_id=commit_ids['ancestor'])
737 743 pull_request.revisions = [commit_ids['change']]
738 744 pull_request.title = u"Test"
739 745 pull_request.description = u"Description"
740 746 pull_request.author = UserModel().get_by_username(
741 747 TEST_USER_ADMIN_LOGIN)
742 748 Session().add(pull_request)
743 749 Session().commit()
744 750 pull_request_id = pull_request.pull_request_id
745 751
746 752 # target has ancestor - ancestor-new
747 753 # source has ancestor - ancestor-new - change-rebased
748 754 backend.pull_heads(target, heads=['ancestor-new'])
749 755 backend.pull_heads(source, heads=['change-rebased'])
750 756
751 757 # update PR
752 758 self.app.post(
753 759 url(controller='pullrequests', action='update',
754 760 repo_name=target.repo_name,
755 761 pull_request_id=str(pull_request_id)),
756 762 params={'update_commits': 'true', '_method': 'put',
757 763 'csrf_token': csrf_token},
758 764 status=200)
759 765
760 766 # Expect the target reference to be updated correctly
761 767 pull_request = PullRequest.get(pull_request_id)
762 768 assert pull_request.revisions == [commit_ids['change-rebased']]
763 769 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
764 770 branch=backend.default_branch_name,
765 771 commit_id=commit_ids['ancestor-new'])
766 772 assert pull_request.target_ref == expected_target_ref
767 773
768 774 def test_remove_pull_request_branch(self, backend_git, csrf_token):
769 775 branch_name = 'development'
770 776 commits = [
771 777 {'message': 'initial-commit'},
772 778 {'message': 'old-feature'},
773 779 {'message': 'new-feature', 'branch': branch_name},
774 780 ]
775 781 repo = backend_git.create_repo(commits)
776 782 commit_ids = backend_git.commit_ids
777 783
778 784 pull_request = PullRequest()
779 785 pull_request.source_repo = repo
780 786 pull_request.target_repo = repo
781 787 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
782 788 branch=branch_name, commit_id=commit_ids['new-feature'])
783 789 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
784 790 branch=backend_git.default_branch_name,
785 791 commit_id=commit_ids['old-feature'])
786 792 pull_request.revisions = [commit_ids['new-feature']]
787 793 pull_request.title = u"Test"
788 794 pull_request.description = u"Description"
789 795 pull_request.author = UserModel().get_by_username(
790 796 TEST_USER_ADMIN_LOGIN)
791 797 Session().add(pull_request)
792 798 Session().commit()
793 799
794 800 vcs = repo.scm_instance()
795 801 vcs.remove_ref('refs/heads/{}'.format(branch_name))
796 802
797 803 response = self.app.get(url(
798 804 controller='pullrequests', action='show',
799 805 repo_name=repo.repo_name,
800 806 pull_request_id=str(pull_request.pull_request_id)))
801 807
802 808 assert response.status_int == 200
803 809 assert_response = AssertResponse(response)
804 810 assert_response.element_contains(
805 811 '#changeset_compare_view_content .alert strong',
806 812 'Missing commits')
807 813 assert_response.element_contains(
808 814 '#changeset_compare_view_content .alert',
809 815 'This pull request cannot be displayed, because one or more'
810 816 ' commits no longer exist in the source repository.')
811 817
812 818 def test_strip_commits_from_pull_request(
813 819 self, backend, pr_util, csrf_token):
814 820 commits = [
815 821 {'message': 'initial-commit'},
816 822 {'message': 'old-feature'},
817 823 {'message': 'new-feature', 'parents': ['initial-commit']},
818 824 ]
819 825 pull_request = pr_util.create_pull_request(
820 826 commits, target_head='initial-commit', source_head='new-feature',
821 827 revisions=['new-feature'])
822 828
823 829 vcs = pr_util.source_repository.scm_instance()
824 830 if backend.alias == 'git':
825 831 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
826 832 else:
827 833 vcs.strip(pr_util.commit_ids['new-feature'])
828 834
829 835 response = self.app.get(url(
830 836 controller='pullrequests', action='show',
831 837 repo_name=pr_util.target_repository.repo_name,
832 838 pull_request_id=str(pull_request.pull_request_id)))
833 839
834 840 assert response.status_int == 200
835 841 assert_response = AssertResponse(response)
836 842 assert_response.element_contains(
837 843 '#changeset_compare_view_content .alert strong',
838 844 'Missing commits')
839 845 assert_response.element_contains(
840 846 '#changeset_compare_view_content .alert',
841 847 'This pull request cannot be displayed, because one or more'
842 848 ' commits no longer exist in the source repository.')
843 849 assert_response.element_contains(
844 850 '#update_commits',
845 851 'Update commits')
846 852
847 853 def test_strip_commits_and_update(
848 854 self, backend, pr_util, csrf_token):
849 855 commits = [
850 856 {'message': 'initial-commit'},
851 857 {'message': 'old-feature'},
852 858 {'message': 'new-feature', 'parents': ['old-feature']},
853 859 ]
854 860 pull_request = pr_util.create_pull_request(
855 861 commits, target_head='old-feature', source_head='new-feature',
856 862 revisions=['new-feature'], mergeable=True)
857 863
858 864 vcs = pr_util.source_repository.scm_instance()
859 865 if backend.alias == 'git':
860 866 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
861 867 else:
862 868 vcs.strip(pr_util.commit_ids['new-feature'])
863 869
864 870 response = self.app.post(
865 871 url(controller='pullrequests', action='update',
866 872 repo_name=pull_request.target_repo.repo_name,
867 873 pull_request_id=str(pull_request.pull_request_id)),
868 874 params={'update_commits': 'true', '_method': 'put',
869 875 'csrf_token': csrf_token})
870 876
871 877 assert response.status_int == 200
872 878 assert response.body == 'true'
873 879
874 880 # Make sure that after update, it won't raise 500 errors
875 881 response = self.app.get(url(
876 882 controller='pullrequests', action='show',
877 883 repo_name=pr_util.target_repository.repo_name,
878 884 pull_request_id=str(pull_request.pull_request_id)))
879 885
880 886 assert response.status_int == 200
881 887 assert_response = AssertResponse(response)
882 888 assert_response.element_contains(
883 889 '#changeset_compare_view_content .alert strong',
884 890 'Missing commits')
885 891
886 892 def test_branch_is_a_link(self, pr_util):
887 893 pull_request = pr_util.create_pull_request()
888 894 pull_request.source_ref = 'branch:origin:1234567890abcdef'
889 895 pull_request.target_ref = 'branch:target:abcdef1234567890'
890 896 Session().add(pull_request)
891 897 Session().commit()
892 898
893 899 response = self.app.get(url(
894 900 controller='pullrequests', action='show',
895 901 repo_name=pull_request.target_repo.scm_instance().name,
896 902 pull_request_id=str(pull_request.pull_request_id)))
897 903 assert response.status_int == 200
898 904 assert_response = AssertResponse(response)
899 905
900 906 origin = assert_response.get_element('.pr-origininfo .tag')
901 907 origin_children = origin.getchildren()
902 908 assert len(origin_children) == 1
903 909 target = assert_response.get_element('.pr-targetinfo .tag')
904 910 target_children = target.getchildren()
905 911 assert len(target_children) == 1
906 912
907 913 expected_origin_link = url(
908 914 'changelog_home',
909 915 repo_name=pull_request.source_repo.scm_instance().name,
910 916 branch='origin')
911 917 expected_target_link = url(
912 918 'changelog_home',
913 919 repo_name=pull_request.target_repo.scm_instance().name,
914 920 branch='target')
915 921 assert origin_children[0].attrib['href'] == expected_origin_link
916 922 assert origin_children[0].text == 'branch: origin'
917 923 assert target_children[0].attrib['href'] == expected_target_link
918 924 assert target_children[0].text == 'branch: target'
919 925
920 926 def test_bookmark_is_not_a_link(self, pr_util):
921 927 pull_request = pr_util.create_pull_request()
922 928 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
923 929 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
924 930 Session().add(pull_request)
925 931 Session().commit()
926 932
927 933 response = self.app.get(url(
928 934 controller='pullrequests', action='show',
929 935 repo_name=pull_request.target_repo.scm_instance().name,
930 936 pull_request_id=str(pull_request.pull_request_id)))
931 937 assert response.status_int == 200
932 938 assert_response = AssertResponse(response)
933 939
934 940 origin = assert_response.get_element('.pr-origininfo .tag')
935 941 assert origin.text.strip() == 'bookmark: origin'
936 942 assert origin.getchildren() == []
937 943
938 944 target = assert_response.get_element('.pr-targetinfo .tag')
939 945 assert target.text.strip() == 'bookmark: target'
940 946 assert target.getchildren() == []
941 947
942 948 def test_tag_is_not_a_link(self, pr_util):
943 949 pull_request = pr_util.create_pull_request()
944 950 pull_request.source_ref = 'tag:origin:1234567890abcdef'
945 951 pull_request.target_ref = 'tag:target:abcdef1234567890'
946 952 Session().add(pull_request)
947 953 Session().commit()
948 954
949 955 response = self.app.get(url(
950 956 controller='pullrequests', action='show',
951 957 repo_name=pull_request.target_repo.scm_instance().name,
952 958 pull_request_id=str(pull_request.pull_request_id)))
953 959 assert response.status_int == 200
954 960 assert_response = AssertResponse(response)
955 961
956 962 origin = assert_response.get_element('.pr-origininfo .tag')
957 963 assert origin.text.strip() == 'tag: origin'
958 964 assert origin.getchildren() == []
959 965
960 966 target = assert_response.get_element('.pr-targetinfo .tag')
961 967 assert target.text.strip() == 'tag: target'
962 968 assert target.getchildren() == []
963 969
964 970 def test_description_is_escaped_on_index_page(self, backend, pr_util):
965 971 xss_description = "<script>alert('Hi!')</script>"
966 972 pull_request = pr_util.create_pull_request(description=xss_description)
967 973 response = self.app.get(url(
968 974 controller='pullrequests', action='show_all',
969 975 repo_name=pull_request.target_repo.repo_name))
970 976 response.mustcontain(
971 977 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
972 978
973 979 @pytest.mark.parametrize('mergeable', [True, False])
974 980 def test_shadow_repository_link(
975 981 self, mergeable, pr_util, http_host_stub):
976 982 """
977 983 Check that the pull request summary page displays a link to the shadow
978 984 repository if the pull request is mergeable. If it is not mergeable
979 985 the link should not be displayed.
980 986 """
981 987 pull_request = pr_util.create_pull_request(
982 988 mergeable=mergeable, enable_notifications=False)
983 989 target_repo = pull_request.target_repo.scm_instance()
984 990 pr_id = pull_request.pull_request_id
985 991 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
986 992 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
987 993
988 994 response = self.app.get(url(
989 995 controller='pullrequests', action='show',
990 996 repo_name=target_repo.name,
991 997 pull_request_id=str(pr_id)))
992 998
993 999 assertr = AssertResponse(response)
994 1000 if mergeable:
995 1001 assertr.element_value_contains(
996 1002 'div.pr-mergeinfo input', shadow_url)
997 1003 assertr.element_value_contains(
998 1004 'div.pr-mergeinfo input', 'pr-merge')
999 1005 else:
1000 1006 assertr.no_element_exists('div.pr-mergeinfo')
1001 1007
1002 1008
1003 1009 @pytest.mark.usefixtures('app')
1004 1010 @pytest.mark.backends("git", "hg")
1005 1011 class TestPullrequestsControllerDelete(object):
1006 1012 def test_pull_request_delete_button_permissions_admin(
1007 1013 self, autologin_user, user_admin, pr_util):
1008 1014 pull_request = pr_util.create_pull_request(
1009 1015 author=user_admin.username, enable_notifications=False)
1010 1016
1011 1017 response = self.app.get(url(
1012 1018 controller='pullrequests', action='show',
1013 1019 repo_name=pull_request.target_repo.scm_instance().name,
1014 1020 pull_request_id=str(pull_request.pull_request_id)))
1015 1021
1016 1022 response.mustcontain('id="delete_pullrequest"')
1017 1023 response.mustcontain('Confirm to delete this pull request')
1018 1024
1019 1025 def test_pull_request_delete_button_permissions_owner(
1020 1026 self, autologin_regular_user, user_regular, pr_util):
1021 1027 pull_request = pr_util.create_pull_request(
1022 1028 author=user_regular.username, enable_notifications=False)
1023 1029
1024 1030 response = self.app.get(url(
1025 1031 controller='pullrequests', action='show',
1026 1032 repo_name=pull_request.target_repo.scm_instance().name,
1027 1033 pull_request_id=str(pull_request.pull_request_id)))
1028 1034
1029 1035 response.mustcontain('id="delete_pullrequest"')
1030 1036 response.mustcontain('Confirm to delete this pull request')
1031 1037
1032 1038 def test_pull_request_delete_button_permissions_forbidden(
1033 1039 self, autologin_regular_user, user_regular, user_admin, pr_util):
1034 1040 pull_request = pr_util.create_pull_request(
1035 1041 author=user_admin.username, enable_notifications=False)
1036 1042
1037 1043 response = self.app.get(url(
1038 1044 controller='pullrequests', action='show',
1039 1045 repo_name=pull_request.target_repo.scm_instance().name,
1040 1046 pull_request_id=str(pull_request.pull_request_id)))
1041 1047 response.mustcontain(no=['id="delete_pullrequest"'])
1042 1048 response.mustcontain(no=['Confirm to delete this pull request'])
1043 1049
1044 1050 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1045 1051 self, autologin_regular_user, user_regular, user_admin, pr_util,
1046 1052 user_util):
1047 1053
1048 1054 pull_request = pr_util.create_pull_request(
1049 1055 author=user_admin.username, enable_notifications=False)
1050 1056
1051 1057 user_util.grant_user_permission_to_repo(
1052 1058 pull_request.target_repo, user_regular,
1053 1059 'repository.write')
1054 1060
1055 1061 response = self.app.get(url(
1056 1062 controller='pullrequests', action='show',
1057 1063 repo_name=pull_request.target_repo.scm_instance().name,
1058 1064 pull_request_id=str(pull_request.pull_request_id)))
1059 1065
1060 1066 response.mustcontain('id="open_edit_pullrequest"')
1061 1067 response.mustcontain('id="delete_pullrequest"')
1062 1068 response.mustcontain(no=['Confirm to delete this pull request'])
1063 1069
1064 1070
1065 1071 def assert_pull_request_status(pull_request, expected_status):
1066 1072 status = ChangesetStatusModel().calculated_review_status(
1067 1073 pull_request=pull_request)
1068 1074 assert status == expected_status
1069 1075
1070 1076
1071 1077 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1072 1078 @pytest.mark.usefixtures("autologin_user")
1073 1079 def test_redirects_to_repo_summary_for_svn_repositories(
1074 1080 backend_svn, app, action):
1075 1081 denied_actions = ['show_all', 'index', 'create']
1076 1082 for action in denied_actions:
1077 1083 response = app.get(url(
1078 1084 controller='pullrequests', action=action,
1079 1085 repo_name=backend_svn.repo_name))
1080 1086 assert response.status_int == 302
1081 1087
1082 1088 # Not allowed, redirect to the summary
1083 1089 redirected = response.follow()
1084 1090 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1085 1091
1086 1092 # URL adds leading slash and path doesn't have it
1087 1093 assert redirected.req.path == summary_url
1088 1094
1089 1095
1090 1096 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1091 1097 # TODO: johbo: Global import not possible because models.forms blows up
1092 1098 from rhodecode.controllers.pullrequests import PullrequestsController
1093 1099 controller = PullrequestsController()
1094 1100 patcher = mock.patch(
1095 1101 'rhodecode.model.db.BaseModel.get', return_value=None)
1096 1102 with pytest.raises(HTTPNotFound), patcher:
1097 1103 controller._delete_comment(1)
@@ -1,144 +1,141 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23
23 from rhodecode.model.db import Session, UserLog
24 24 from rhodecode.lib import hooks_base, utils2
25 25
26 26
27 @mock.patch.multiple(
28 hooks_base,
29 action_logger=mock.Mock(),
30 post_push_extension=mock.Mock(),
31 Repository=mock.Mock())
32 27 def test_post_push_truncates_commits(user_regular, repo_stub):
33 28 extras = {
34 29 'ip': '127.0.0.1',
35 30 'username': user_regular.username,
36 31 'action': 'push_local',
37 32 'repository': repo_stub.repo_name,
38 33 'scm': 'git',
39 34 'config': '',
40 35 'server_url': 'http://example.com',
41 36 'make_lock': None,
42 37 'user_agent': 'some-client',
43 38 'locked_by': [None],
44 39 'commit_ids': ['abcde12345' * 4] * 30000,
45 40 'is_shadow_repo': False,
46 41 }
47 42 extras = utils2.AttributeDict(extras)
48 43
49 44 hooks_base.post_push(extras)
50 45
51 46 # Calculate appropriate action string here
52 expected_action = 'push_local:%s' % ','.join(extras.commit_ids[:29000])
47 commit_ids = extras.commit_ids[:10000]
53 48
54 hooks_base.action_logger.assert_called_with(
55 extras.username, expected_action, extras.repository, extras.ip,
56 commit=True)
49 entry = UserLog.query().order_by('-user_log_id').first()
50 assert entry.action == 'user.push'
51 assert entry.action_data['commit_ids'] == commit_ids
52 Session().delete(entry)
53 Session().commit()
57 54
58 55
59 56 def assert_called_with_mock(callable_, expected_mock_name):
60 57 mock_obj = callable_.call_args[0][0]
61 58 mock_name = mock_obj._mock_new_parent._mock_new_name
62 59 assert mock_name == expected_mock_name
63 60
64 61
65 62 @pytest.fixture
66 63 def hook_extras(user_regular, repo_stub):
67 64 extras = utils2.AttributeDict({
68 65 'ip': '127.0.0.1',
69 66 'username': user_regular.username,
70 67 'action': 'push',
71 68 'repository': repo_stub.repo_name,
72 69 'scm': '',
73 70 'config': '',
74 71 'server_url': 'http://example.com',
75 72 'make_lock': None,
76 73 'user_agent': 'some-client',
77 74 'locked_by': [None],
78 75 'commit_ids': [],
79 76 'is_shadow_repo': False,
80 77 })
81 78 return extras
82 79
83 80
84 81 @pytest.mark.parametrize('func, extension, event', [
85 82 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
86 83 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
87 84 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
88 85 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
89 86 ])
90 87 def test_hooks_propagate(func, extension, event, hook_extras):
91 88 """
92 89 Tests that our hook code propagates to rhodecode extensions and triggers
93 90 the appropriate event.
94 91 """
95 92 extension_mock = mock.Mock()
96 93 events_mock = mock.Mock()
97 94 patches = {
98 95 'Repository': mock.Mock(),
99 96 'events': events_mock,
100 97 extension: extension_mock,
101 98 }
102 99
103 100 # Clear shadow repo flag.
104 101 hook_extras.is_shadow_repo = False
105 102
106 103 # Execute hook function.
107 104 with mock.patch.multiple(hooks_base, **patches):
108 105 func(hook_extras)
109 106
110 107 # Assert that extensions are called and event was fired.
111 108 extension_mock.called_once()
112 109 assert_called_with_mock(events_mock.trigger, event)
113 110
114 111
115 112 @pytest.mark.parametrize('func, extension, event', [
116 113 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
117 114 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
118 115 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
119 116 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
120 117 ])
121 118 def test_hooks_propagates_not_on_shadow(func, extension, event, hook_extras):
122 119 """
123 120 If hooks are called by a request to a shadow repo we only want to run our
124 121 internal hooks code but not external ones like rhodecode extensions or
125 122 trigger an event.
126 123 """
127 124 extension_mock = mock.Mock()
128 125 events_mock = mock.Mock()
129 126 patches = {
130 127 'Repository': mock.Mock(),
131 128 'events': events_mock,
132 129 extension: extension_mock,
133 130 }
134 131
135 132 # Set shadow repo flag.
136 133 hook_extras.is_shadow_repo = True
137 134
138 135 # Execute hook function.
139 136 with mock.patch.multiple(hooks_base, **patches):
140 137 func(hook_extras)
141 138
142 139 # Assert that extensions are *not* called and event was *not* fired.
143 140 assert not extension_mock.called
144 141 assert not events_mock.trigger.called
General Comments 0
You need to be logged in to leave comments. Login now