##// END OF EJS Templates
security: make sure the admin of repo can only delete comments which are from the same repo....
ergo -
r1818:1ced1b24 default
parent child Browse files
Show More
@@ -1,484 +1,488 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 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode, safe_int
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import CommentsModel
50 50 from rhodecode.model.meta import Session
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def _update_with_GET(params, GET):
57 57 for k in ['diff1', 'diff2', 'diff']:
58 58 params[k] += GET.getall(k)
59 59
60 60
61 61 def get_ignore_ws(fid, GET):
62 62 ig_ws_global = GET.get('ignorews')
63 63 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 64 if ig_ws:
65 65 try:
66 66 return int(ig_ws[0].split(':')[-1])
67 67 except Exception:
68 68 pass
69 69 return ig_ws_global
70 70
71 71
72 72 def _ignorews_url(GET, fileid=None):
73 73 fileid = str(fileid) if fileid else None
74 74 params = defaultdict(list)
75 75 _update_with_GET(params, GET)
76 76 label = _('Show whitespace')
77 77 tooltiplbl = _('Show whitespace for all diffs')
78 78 ig_ws = get_ignore_ws(fileid, GET)
79 79 ln_ctx = get_line_ctx(fileid, GET)
80 80
81 81 if ig_ws is None:
82 82 params['ignorews'] += [1]
83 83 label = _('Ignore whitespace')
84 84 tooltiplbl = _('Ignore whitespace for all diffs')
85 85 ctx_key = 'context'
86 86 ctx_val = ln_ctx
87 87
88 88 # if we have passed in ln_ctx pass it along to our params
89 89 if ln_ctx:
90 90 params[ctx_key] += [ctx_val]
91 91
92 92 if fileid:
93 93 params['anchor'] = 'a_' + fileid
94 94 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 95
96 96
97 97 def get_line_ctx(fid, GET):
98 98 ln_ctx_global = GET.get('context')
99 99 if fid:
100 100 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 101 else:
102 102 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 103 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 104 if ln_ctx:
105 105 ln_ctx = [ln_ctx]
106 106
107 107 if ln_ctx:
108 108 retval = ln_ctx[0].split(':')[-1]
109 109 else:
110 110 retval = ln_ctx_global
111 111
112 112 try:
113 113 return int(retval)
114 114 except Exception:
115 115 return 3
116 116
117 117
118 118 def _context_url(GET, fileid=None):
119 119 """
120 120 Generates a url for context lines.
121 121
122 122 :param fileid:
123 123 """
124 124
125 125 fileid = str(fileid) if fileid else None
126 126 ig_ws = get_ignore_ws(fileid, GET)
127 127 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 128
129 129 params = defaultdict(list)
130 130 _update_with_GET(params, GET)
131 131
132 132 if ln_ctx > 0:
133 133 params['context'] += [ln_ctx]
134 134
135 135 if ig_ws:
136 136 ig_ws_key = 'ignorews'
137 137 ig_ws_val = 1
138 138 params[ig_ws_key] += [ig_ws_val]
139 139
140 140 lbl = _('Increase context')
141 141 tooltiplbl = _('Increase context for all diffs')
142 142
143 143 if fileid:
144 144 params['anchor'] = 'a_' + fileid
145 145 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 146
147 147
148 148 class ChangesetController(BaseRepoController):
149 149
150 150 def __before__(self):
151 151 super(ChangesetController, self).__before__()
152 152 c.affected_files_cut_off = 60
153 153
154 154 def _index(self, commit_id_range, method):
155 155 c.ignorews_url = _ignorews_url
156 156 c.context_url = _context_url
157 157 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 158
159 159 # fetch global flags of ignore ws or context lines
160 160 context_lcl = get_line_ctx('', request.GET)
161 161 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 162
163 163 # diff_limit will cut off the whole diff if the limit is applied
164 164 # otherwise it will just hide the big files from the front-end
165 165 diff_limit = self.cut_off_limit_diff
166 166 file_limit = self.cut_off_limit_file
167 167
168 168 # get ranges of commit ids if preset
169 169 commit_range = commit_id_range.split('...')[:2]
170 170
171 171 try:
172 172 pre_load = ['affected_files', 'author', 'branch', 'date',
173 173 'message', 'parents']
174 174
175 175 if len(commit_range) == 2:
176 176 commits = c.rhodecode_repo.get_commits(
177 177 start_id=commit_range[0], end_id=commit_range[1],
178 178 pre_load=pre_load)
179 179 commits = list(commits)
180 180 else:
181 181 commits = [c.rhodecode_repo.get_commit(
182 182 commit_id=commit_id_range, pre_load=pre_load)]
183 183
184 184 c.commit_ranges = commits
185 185 if not c.commit_ranges:
186 186 raise RepositoryError(
187 187 'The commit range returned an empty result')
188 188 except CommitDoesNotExistError:
189 189 msg = _('No such commit exists for this repository')
190 190 h.flash(msg, category='error')
191 191 raise HTTPNotFound()
192 192 except Exception:
193 193 log.exception("General failure")
194 194 raise HTTPNotFound()
195 195
196 196 c.changes = OrderedDict()
197 197 c.lines_added = 0
198 198 c.lines_deleted = 0
199 199
200 200 # auto collapse if we have more than limit
201 201 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 202 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 203
204 204 c.commit_statuses = ChangesetStatus.STATUSES
205 205 c.inline_comments = []
206 206 c.files = []
207 207
208 208 c.statuses = []
209 209 c.comments = []
210 210 c.unresolved_comments = []
211 211 if len(c.commit_ranges) == 1:
212 212 commit = c.commit_ranges[0]
213 213 c.comments = CommentsModel().get_comments(
214 214 c.rhodecode_db_repo.repo_id,
215 215 revision=commit.raw_id)
216 216 c.statuses.append(ChangesetStatusModel().get_status(
217 217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 218 # comments from PR
219 219 statuses = ChangesetStatusModel().get_statuses(
220 220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 221 with_revisions=True)
222 222 prs = set(st.pull_request for st in statuses
223 223 if st.pull_request is not None)
224 224 # from associated statuses, check the pull requests, and
225 225 # show comments from them
226 226 for pr in prs:
227 227 c.comments.extend(pr.comments)
228 228
229 229 c.unresolved_comments = CommentsModel()\
230 230 .get_commit_unresolved_todos(commit.raw_id)
231 231
232 232 # Iterate over ranges (default commit view is always one commit)
233 233 for commit in c.commit_ranges:
234 234 c.changes[commit.raw_id] = []
235 235
236 236 commit2 = commit
237 237 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
238 238
239 239 _diff = c.rhodecode_repo.get_diff(
240 240 commit1, commit2,
241 241 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
242 242 diff_processor = diffs.DiffProcessor(
243 243 _diff, format='newdiff', diff_limit=diff_limit,
244 244 file_limit=file_limit, show_full_diff=fulldiff)
245 245
246 246 commit_changes = OrderedDict()
247 247 if method == 'show':
248 248 _parsed = diff_processor.prepare()
249 249 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
250 250
251 251 _parsed = diff_processor.prepare()
252 252
253 253 def _node_getter(commit):
254 254 def get_node(fname):
255 255 try:
256 256 return commit.get_node(fname)
257 257 except NodeDoesNotExistError:
258 258 return None
259 259 return get_node
260 260
261 261 inline_comments = CommentsModel().get_inline_comments(
262 262 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
263 263 c.inline_cnt = CommentsModel().get_inline_comments_count(
264 264 inline_comments)
265 265
266 266 diffset = codeblocks.DiffSet(
267 267 repo_name=c.repo_name,
268 268 source_node_getter=_node_getter(commit1),
269 269 target_node_getter=_node_getter(commit2),
270 270 comments=inline_comments
271 271 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
272 272 c.changes[commit.raw_id] = diffset
273 273 else:
274 274 # downloads/raw we only need RAW diff nothing else
275 275 diff = diff_processor.as_raw()
276 276 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
277 277
278 278 # sort comments by how they were generated
279 279 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
280 280
281 281 if len(c.commit_ranges) == 1:
282 282 c.commit = c.commit_ranges[0]
283 283 c.parent_tmpl = ''.join(
284 284 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
285 285 if method == 'download':
286 286 response.content_type = 'text/plain'
287 287 response.content_disposition = (
288 288 'attachment; filename=%s.diff' % commit_id_range[:12])
289 289 return diff
290 290 elif method == 'patch':
291 291 response.content_type = 'text/plain'
292 292 c.diff = safe_unicode(diff)
293 293 return render('changeset/patch_changeset.mako')
294 294 elif method == 'raw':
295 295 response.content_type = 'text/plain'
296 296 return diff
297 297 elif method == 'show':
298 298 if len(c.commit_ranges) == 1:
299 299 return render('changeset/changeset.mako')
300 300 else:
301 301 c.ancestor = None
302 302 c.target_repo = c.rhodecode_db_repo
303 303 return render('changeset/changeset_range.mako')
304 304
305 305 @LoginRequired()
306 306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 307 'repository.admin')
308 308 def index(self, revision, method='show'):
309 309 return self._index(revision, method=method)
310 310
311 311 @LoginRequired()
312 312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 313 'repository.admin')
314 314 def changeset_raw(self, revision):
315 315 return self._index(revision, method='raw')
316 316
317 317 @LoginRequired()
318 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 319 'repository.admin')
320 320 def changeset_patch(self, revision):
321 321 return self._index(revision, method='patch')
322 322
323 323 @LoginRequired()
324 324 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 325 'repository.admin')
326 326 def changeset_download(self, revision):
327 327 return self._index(revision, method='download')
328 328
329 329 @LoginRequired()
330 330 @NotAnonymous()
331 331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 332 'repository.admin')
333 333 @auth.CSRFRequired()
334 334 @jsonify
335 335 def comment(self, repo_name, revision):
336 336 commit_id = revision
337 337 status = request.POST.get('changeset_status', None)
338 338 text = request.POST.get('text')
339 339 comment_type = request.POST.get('comment_type')
340 340 resolves_comment_id = request.POST.get('resolves_comment_id', None)
341 341
342 342 if status:
343 343 text = text or (_('Status change %(transition_icon)s %(status)s')
344 344 % {'transition_icon': '>',
345 345 'status': ChangesetStatus.get_status_lbl(status)})
346 346
347 347 multi_commit_ids = []
348 348 for _commit_id in request.POST.get('commit_ids', '').split(','):
349 349 if _commit_id not in ['', None, EmptyCommit.raw_id]:
350 350 if _commit_id not in multi_commit_ids:
351 351 multi_commit_ids.append(_commit_id)
352 352
353 353 commit_ids = multi_commit_ids or [commit_id]
354 354
355 355 comment = None
356 356 for current_id in filter(None, commit_ids):
357 357 c.co = comment = CommentsModel().create(
358 358 text=text,
359 359 repo=c.rhodecode_db_repo.repo_id,
360 360 user=c.rhodecode_user.user_id,
361 361 commit_id=current_id,
362 362 f_path=request.POST.get('f_path'),
363 363 line_no=request.POST.get('line'),
364 364 status_change=(ChangesetStatus.get_status_lbl(status)
365 365 if status else None),
366 366 status_change_type=status,
367 367 comment_type=comment_type,
368 368 resolves_comment_id=resolves_comment_id
369 369 )
370 370
371 371 # get status if set !
372 372 if status:
373 373 # if latest status was from pull request and it's closed
374 374 # disallow changing status !
375 375 # dont_allow_on_closed_pull_request = True !
376 376
377 377 try:
378 378 ChangesetStatusModel().set_status(
379 379 c.rhodecode_db_repo.repo_id,
380 380 status,
381 381 c.rhodecode_user.user_id,
382 382 comment,
383 383 revision=current_id,
384 384 dont_allow_on_closed_pull_request=True
385 385 )
386 386 except StatusChangeOnClosedPullRequestError:
387 387 msg = _('Changing the status of a commit associated with '
388 388 'a closed pull request is not allowed')
389 389 log.exception(msg)
390 390 h.flash(msg, category='warning')
391 391 return redirect(h.url(
392 392 'changeset_home', repo_name=repo_name,
393 393 revision=current_id))
394 394
395 395 # finalize, commit and redirect
396 396 Session().commit()
397 397
398 398 data = {
399 399 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
400 400 }
401 401 if comment:
402 402 data.update(comment.get_dict())
403 403 data.update({'rendered_text':
404 404 render('changeset/changeset_comment_block.mako')})
405 405
406 406 return data
407 407
408 408 @LoginRequired()
409 409 @NotAnonymous()
410 410 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
411 411 'repository.admin')
412 412 @auth.CSRFRequired()
413 413 def preview_comment(self):
414 414 # Technically a CSRF token is not needed as no state changes with this
415 415 # call. However, as this is a POST is better to have it, so automated
416 416 # tools don't flag it as potential CSRF.
417 417 # Post is required because the payload could be bigger than the maximum
418 418 # allowed by GET.
419 419 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
420 420 raise HTTPBadRequest()
421 421 text = request.POST.get('text')
422 422 renderer = request.POST.get('renderer') or 'rst'
423 423 if text:
424 424 return h.render(text, renderer=renderer, mentions=True)
425 425 return ''
426 426
427 427 @LoginRequired()
428 428 @NotAnonymous()
429 429 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
430 430 'repository.admin')
431 431 @auth.CSRFRequired()
432 432 @jsonify
433 433 def delete_comment(self, repo_name, comment_id):
434 comment = ChangesetComment.get(comment_id)
434 comment = ChangesetComment.get_or_404(safe_int(comment_id))
435 435 if not comment:
436 436 log.debug('Comment with id:%s not found, skipping', comment_id)
437 437 # comment already deleted in another call probably
438 438 return True
439 439
440 owner = (comment.author.user_id == c.rhodecode_user.user_id)
441 440 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
442 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
441 super_admin = h.HasPermissionAny('hg.admin')()
442 comment_owner = (comment.author.user_id == c.rhodecode_user.user_id)
443 is_repo_comment = comment.repo.repo_name == c.repo_name
444 comment_repo_admin = is_repo_admin and is_repo_comment
445
446 if super_admin or comment_owner or comment_repo_admin:
443 447 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
444 448 Session().commit()
445 449 return True
446 450 else:
447 451 raise HTTPForbidden()
448 452
449 453 @LoginRequired()
450 454 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
451 455 'repository.admin')
452 456 @jsonify
453 457 def changeset_info(self, repo_name, revision):
454 458 if request.is_xhr:
455 459 try:
456 460 return c.rhodecode_repo.get_commit(commit_id=revision)
457 461 except CommitDoesNotExistError as e:
458 462 return EmptyCommit(message=str(e))
459 463 else:
460 464 raise HTTPBadRequest()
461 465
462 466 @LoginRequired()
463 467 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
464 468 'repository.admin')
465 469 @jsonify
466 470 def changeset_children(self, repo_name, revision):
467 471 if request.is_xhr:
468 472 commit = c.rhodecode_repo.get_commit(commit_id=revision)
469 473 result = {"results": commit.children}
470 474 return result
471 475 else:
472 476 raise HTTPBadRequest()
473 477
474 478 @LoginRequired()
475 479 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
476 480 'repository.admin')
477 481 @jsonify
478 482 def changeset_parents(self, repo_name, revision):
479 483 if request.is_xhr:
480 484 commit = c.rhodecode_repo.get_commit(commit_id=revision)
481 485 result = {"results": commit.parents}
482 486 return result
483 487 else:
484 488 raise HTTPBadRequest()
@@ -1,1011 +1,1016 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-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 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import peppercorn
25 25 import formencode
26 26 import logging
27 27 import collections
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 30 from pylons import request, tmpl_context as c, url
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from pyramid.threadlocal import get_current_registry
34 34 from pyramid.httpexceptions import HTTPFound
35 35 from sqlalchemy.sql import func
36 36 from sqlalchemy.sql.expression import or_
37 37
38 38 from rhodecode import events
39 39 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.base import (
42 42 BaseRepoController, render, vcs_operation_context)
43 43 from rhodecode.lib.auth import (
44 44 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 45 HasAcceptedRepoType, XHRRequired)
46 46 from rhodecode.lib.channelstream import channelstream_request
47 47 from rhodecode.lib.utils import jsonify
48 48 from rhodecode.lib.utils2 import (
49 49 safe_int, safe_str, str2bool, safe_unicode)
50 50 from rhodecode.lib.vcs.backends.base import (
51 51 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 52 from rhodecode.lib.vcs.exceptions import (
53 53 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 54 NodeDoesNotExistError)
55 55
56 56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 57 from rhodecode.model.comment import CommentsModel
58 58 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 59 Repository, PullRequestVersion)
60 60 from rhodecode.model.forms import PullRequestForm
61 61 from rhodecode.model.meta import Session
62 62 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class PullrequestsController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 72 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 73
74 74 @LoginRequired()
75 75 @NotAnonymous()
76 76 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 77 'repository.admin')
78 78 @HasAcceptedRepoType('git', 'hg')
79 79 def index(self):
80 80 source_repo = c.rhodecode_db_repo
81 81
82 82 try:
83 83 source_repo.scm_instance().get_commit()
84 84 except EmptyRepositoryError:
85 85 h.flash(h.literal(_('There are no commits yet')),
86 86 category='warning')
87 87 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88 88
89 89 commit_id = request.GET.get('commit')
90 90 branch_ref = request.GET.get('branch')
91 91 bookmark_ref = request.GET.get('bookmark')
92 92
93 93 try:
94 94 source_repo_data = PullRequestModel().generate_repo_data(
95 95 source_repo, commit_id=commit_id,
96 96 branch=branch_ref, bookmark=bookmark_ref)
97 97 except CommitDoesNotExistError as e:
98 98 log.exception(e)
99 99 h.flash(_('Commit does not exist'), 'error')
100 100 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 101
102 102 default_target_repo = source_repo
103 103
104 104 if source_repo.parent:
105 105 parent_vcs_obj = source_repo.parent.scm_instance()
106 106 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 107 # change default if we have a parent repo
108 108 default_target_repo = source_repo.parent
109 109
110 110 target_repo_data = PullRequestModel().generate_repo_data(
111 111 default_target_repo)
112 112
113 113 selected_source_ref = source_repo_data['refs']['selected_ref']
114 114
115 115 title_source_ref = selected_source_ref.split(':', 2)[1]
116 116 c.default_title = PullRequestModel().generate_pullrequest_title(
117 117 source=source_repo.repo_name,
118 118 source_ref=title_source_ref,
119 119 target=default_target_repo.repo_name
120 120 )
121 121
122 122 c.default_repo_data = {
123 123 'source_repo_name': source_repo.repo_name,
124 124 'source_refs_json': json.dumps(source_repo_data),
125 125 'target_repo_name': default_target_repo.repo_name,
126 126 'target_refs_json': json.dumps(target_repo_data),
127 127 }
128 128 c.default_source_ref = selected_source_ref
129 129
130 130 return render('/pullrequests/pullrequest.mako')
131 131
132 132 @LoginRequired()
133 133 @NotAnonymous()
134 134 @XHRRequired()
135 135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 136 'repository.admin')
137 137 @jsonify
138 138 def get_repo_refs(self, repo_name, target_repo_name):
139 139 repo = Repository.get_by_repo_name(target_repo_name)
140 140 if not repo:
141 141 raise HTTPNotFound
142 142 return PullRequestModel().generate_repo_data(repo)
143 143
144 144 @LoginRequired()
145 145 @NotAnonymous()
146 146 @XHRRequired()
147 147 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 148 'repository.admin')
149 149 @jsonify
150 150 def get_repo_destinations(self, repo_name):
151 151 repo = Repository.get_by_repo_name(repo_name)
152 152 if not repo:
153 153 raise HTTPNotFound
154 154 filter_query = request.GET.get('query')
155 155
156 156 query = Repository.query() \
157 157 .order_by(func.length(Repository.repo_name)) \
158 158 .filter(or_(
159 159 Repository.repo_name == repo.repo_name,
160 160 Repository.fork_id == repo.repo_id))
161 161
162 162 if filter_query:
163 163 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 164 query = query.filter(
165 165 Repository.repo_name.ilike(ilike_expression))
166 166
167 167 add_parent = False
168 168 if repo.parent:
169 169 if filter_query in repo.parent.repo_name:
170 170 parent_vcs_obj = repo.parent.scm_instance()
171 171 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 172 add_parent = True
173 173
174 174 limit = 20 - 1 if add_parent else 20
175 175 all_repos = query.limit(limit).all()
176 176 if add_parent:
177 177 all_repos += [repo.parent]
178 178
179 179 repos = []
180 180 for obj in self.scm_model.get_repos(all_repos):
181 181 repos.append({
182 182 'id': obj['name'],
183 183 'text': obj['name'],
184 184 'type': 'repo',
185 185 'obj': obj['dbrepo']
186 186 })
187 187
188 188 data = {
189 189 'more': False,
190 190 'results': [{
191 191 'text': _('Repositories'),
192 192 'children': repos
193 193 }] if repos else []
194 194 }
195 195 return data
196 196
197 197 @LoginRequired()
198 198 @NotAnonymous()
199 199 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 200 'repository.admin')
201 201 @HasAcceptedRepoType('git', 'hg')
202 202 @auth.CSRFRequired()
203 203 def create(self, repo_name):
204 204 repo = Repository.get_by_repo_name(repo_name)
205 205 if not repo:
206 206 raise HTTPNotFound
207 207
208 208 controls = peppercorn.parse(request.POST.items())
209 209
210 210 try:
211 211 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 212 except formencode.Invalid as errors:
213 213 if errors.error_dict.get('revisions'):
214 214 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 215 elif errors.error_dict.get('pullrequest_title'):
216 216 msg = _('Pull request requires a title with min. 3 chars')
217 217 else:
218 218 msg = _('Error creating pull request: {}').format(errors)
219 219 log.exception(msg)
220 220 h.flash(msg, 'error')
221 221
222 222 # would rather just go back to form ...
223 223 return redirect(url('pullrequest_home', repo_name=repo_name))
224 224
225 225 source_repo = _form['source_repo']
226 226 source_ref = _form['source_ref']
227 227 target_repo = _form['target_repo']
228 228 target_ref = _form['target_ref']
229 229 commit_ids = _form['revisions'][::-1]
230 230
231 231 # find the ancestor for this pr
232 232 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 233 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 234
235 235 source_scm = source_db_repo.scm_instance()
236 236 target_scm = target_db_repo.scm_instance()
237 237
238 238 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 239 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 240
241 241 ancestor = source_scm.get_common_ancestor(
242 242 source_commit.raw_id, target_commit.raw_id, target_scm)
243 243
244 244 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 245 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 246
247 247 pullrequest_title = _form['pullrequest_title']
248 248 title_source_ref = source_ref.split(':', 2)[1]
249 249 if not pullrequest_title:
250 250 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 251 source=source_repo,
252 252 source_ref=title_source_ref,
253 253 target=target_repo
254 254 )
255 255
256 256 description = _form['pullrequest_desc']
257 257
258 258 get_default_reviewers_data, validate_default_reviewers = \
259 259 PullRequestModel().get_reviewer_functions()
260 260
261 261 # recalculate reviewers logic, to make sure we can validate this
262 262 reviewer_rules = get_default_reviewers_data(
263 263 c.rhodecode_user.get_instance(), source_db_repo,
264 264 source_commit, target_db_repo, target_commit)
265 265
266 266 given_reviewers = _form['review_members']
267 267 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
268 268
269 269 try:
270 270 pull_request = PullRequestModel().create(
271 271 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 272 target_ref, commit_ids, reviewers, pullrequest_title,
273 273 description, reviewer_rules
274 274 )
275 275 Session().commit()
276 276 h.flash(_('Successfully opened new pull request'),
277 277 category='success')
278 278 except Exception as e:
279 279 msg = _('Error occurred during creation of this pull request.')
280 280 log.exception(msg)
281 281 h.flash(msg, category='error')
282 282 return redirect(url('pullrequest_home', repo_name=repo_name))
283 283
284 284 raise HTTPFound(
285 285 h.route_path('pullrequest_show', repo_name=target_repo,
286 286 pull_request_id=pull_request.pull_request_id))
287 287
288 288 @LoginRequired()
289 289 @NotAnonymous()
290 290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 291 'repository.admin')
292 292 @auth.CSRFRequired()
293 293 @jsonify
294 294 def update(self, repo_name, pull_request_id):
295 295 pull_request_id = safe_int(pull_request_id)
296 296 pull_request = PullRequest.get_or_404(pull_request_id)
297 297 # only owner or admin can update it
298 298 allowed_to_update = PullRequestModel().check_user_update(
299 299 pull_request, c.rhodecode_user)
300 300 if allowed_to_update:
301 301 controls = peppercorn.parse(request.POST.items())
302 302
303 303 if 'review_members' in controls:
304 304 self._update_reviewers(
305 305 pull_request_id, controls['review_members'],
306 306 pull_request.reviewer_data)
307 307 elif str2bool(request.POST.get('update_commits', 'false')):
308 308 self._update_commits(pull_request)
309 309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
310 310 self._edit_pull_request(pull_request)
311 311 else:
312 312 raise HTTPBadRequest()
313 313 return True
314 314 raise HTTPForbidden()
315 315
316 316 def _edit_pull_request(self, pull_request):
317 317 try:
318 318 PullRequestModel().edit(
319 319 pull_request, request.POST.get('title'),
320 320 request.POST.get('description'), c.rhodecode_user)
321 321 except ValueError:
322 322 msg = _(u'Cannot update closed pull requests.')
323 323 h.flash(msg, category='error')
324 324 return
325 325 else:
326 326 Session().commit()
327 327
328 328 msg = _(u'Pull request title & description updated.')
329 329 h.flash(msg, category='success')
330 330 return
331 331
332 332 def _update_commits(self, pull_request):
333 333 resp = PullRequestModel().update_commits(pull_request)
334 334
335 335 if resp.executed:
336 336
337 337 if resp.target_changed and resp.source_changed:
338 338 changed = 'target and source repositories'
339 339 elif resp.target_changed and not resp.source_changed:
340 340 changed = 'target repository'
341 341 elif not resp.target_changed and resp.source_changed:
342 342 changed = 'source repository'
343 343 else:
344 344 changed = 'nothing'
345 345
346 346 msg = _(
347 347 u'Pull request updated to "{source_commit_id}" with '
348 348 u'{count_added} added, {count_removed} removed commits. '
349 349 u'Source of changes: {change_source}')
350 350 msg = msg.format(
351 351 source_commit_id=pull_request.source_ref_parts.commit_id,
352 352 count_added=len(resp.changes.added),
353 353 count_removed=len(resp.changes.removed),
354 354 change_source=changed)
355 355 h.flash(msg, category='success')
356 356
357 357 registry = get_current_registry()
358 358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 360 if channelstream_config.get('enabled'):
361 361 message = msg + (
362 362 ' - <a onclick="window.location.reload()">'
363 363 '<strong>{}</strong></a>'.format(_('Reload page')))
364 364 channel = '/repo${}$/pr/{}'.format(
365 365 pull_request.target_repo.repo_name,
366 366 pull_request.pull_request_id
367 367 )
368 368 payload = {
369 369 'type': 'message',
370 370 'user': 'system',
371 371 'exclude_users': [request.user.username],
372 372 'channel': channel,
373 373 'message': {
374 374 'message': message,
375 375 'level': 'success',
376 376 'topic': '/notifications'
377 377 }
378 378 }
379 379 channelstream_request(
380 380 channelstream_config, [payload], '/message',
381 381 raise_exc=False)
382 382 else:
383 383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
384 384 warning_reasons = [
385 385 UpdateFailureReason.NO_CHANGE,
386 386 UpdateFailureReason.WRONG_REF_TYPE,
387 387 ]
388 388 category = 'warning' if resp.reason in warning_reasons else 'error'
389 389 h.flash(msg, category=category)
390 390
391 391 @auth.CSRFRequired()
392 392 @LoginRequired()
393 393 @NotAnonymous()
394 394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 395 'repository.admin')
396 396 def merge(self, repo_name, pull_request_id):
397 397 """
398 398 POST /{repo_name}/pull-request/{pull_request_id}
399 399
400 400 Merge will perform a server-side merge of the specified
401 401 pull request, if the pull request is approved and mergeable.
402 402 After successful merging, the pull request is automatically
403 403 closed, with a relevant comment.
404 404 """
405 405 pull_request_id = safe_int(pull_request_id)
406 406 pull_request = PullRequest.get_or_404(pull_request_id)
407 407 user = c.rhodecode_user
408 408
409 409 check = MergeCheck.validate(pull_request, user)
410 410 merge_possible = not check.failed
411 411
412 412 for err_type, error_msg in check.errors:
413 413 h.flash(error_msg, category=err_type)
414 414
415 415 if merge_possible:
416 416 log.debug("Pre-conditions checked, trying to merge.")
417 417 extras = vcs_operation_context(
418 418 request.environ, repo_name=pull_request.target_repo.repo_name,
419 419 username=user.username, action='push',
420 420 scm=pull_request.target_repo.repo_type)
421 421 self._merge_pull_request(pull_request, user, extras)
422 422
423 423 raise HTTPFound(
424 424 h.route_path('pullrequest_show',
425 425 repo_name=pull_request.target_repo.repo_name,
426 426 pull_request_id=pull_request.pull_request_id))
427 427
428 428 def _merge_pull_request(self, pull_request, user, extras):
429 429 merge_resp = PullRequestModel().merge(
430 430 pull_request, user, extras=extras)
431 431
432 432 if merge_resp.executed:
433 433 log.debug("The merge was successful, closing the pull request.")
434 434 PullRequestModel().close_pull_request(
435 435 pull_request.pull_request_id, user)
436 436 Session().commit()
437 437 msg = _('Pull request was successfully merged and closed.')
438 438 h.flash(msg, category='success')
439 439 else:
440 440 log.debug(
441 441 "The merge was not successful. Merge response: %s",
442 442 merge_resp)
443 443 msg = PullRequestModel().merge_status_message(
444 444 merge_resp.failure_reason)
445 445 h.flash(msg, category='error')
446 446
447 447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
448 448
449 449 get_default_reviewers_data, validate_default_reviewers = \
450 450 PullRequestModel().get_reviewer_functions()
451 451
452 452 try:
453 453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
454 454 except ValueError as e:
455 455 log.error('Reviewers Validation: {}'.format(e))
456 456 h.flash(e, category='error')
457 457 return
458 458
459 459 PullRequestModel().update_reviewers(
460 460 pull_request_id, reviewers, c.rhodecode_user)
461 461 h.flash(_('Pull request reviewers updated.'), category='success')
462 462 Session().commit()
463 463
464 464 @LoginRequired()
465 465 @NotAnonymous()
466 466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 467 'repository.admin')
468 468 @auth.CSRFRequired()
469 469 @jsonify
470 470 def delete(self, repo_name, pull_request_id):
471 471 pull_request_id = safe_int(pull_request_id)
472 472 pull_request = PullRequest.get_or_404(pull_request_id)
473 473
474 474 pr_closed = pull_request.is_closed()
475 475 allowed_to_delete = PullRequestModel().check_user_delete(
476 476 pull_request, c.rhodecode_user) and not pr_closed
477 477
478 478 # only owner can delete it !
479 479 if allowed_to_delete:
480 480 PullRequestModel().delete(pull_request, c.rhodecode_user)
481 481 Session().commit()
482 482 h.flash(_('Successfully deleted pull request'),
483 483 category='success')
484 484 return redirect(url('my_account_pullrequests'))
485 485
486 486 h.flash(_('Your are not allowed to delete this pull request'),
487 487 category='error')
488 488 raise HTTPForbidden()
489 489
490 490 def _get_pr_version(self, pull_request_id, version=None):
491 491 pull_request_id = safe_int(pull_request_id)
492 492 at_version = None
493 493
494 494 if version and version == 'latest':
495 495 pull_request_ver = PullRequest.get(pull_request_id)
496 496 pull_request_obj = pull_request_ver
497 497 _org_pull_request_obj = pull_request_obj
498 498 at_version = 'latest'
499 499 elif version:
500 500 pull_request_ver = PullRequestVersion.get_or_404(version)
501 501 pull_request_obj = pull_request_ver
502 502 _org_pull_request_obj = pull_request_ver.pull_request
503 503 at_version = pull_request_ver.pull_request_version_id
504 504 else:
505 505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
506 506 pull_request_id)
507 507
508 508 pull_request_display_obj = PullRequest.get_pr_display_object(
509 509 pull_request_obj, _org_pull_request_obj)
510 510
511 511 return _org_pull_request_obj, pull_request_obj, \
512 512 pull_request_display_obj, at_version
513 513
514 514 def _get_diffset(
515 515 self, source_repo, source_ref_id, target_ref_id, target_commit,
516 516 source_commit, diff_limit, file_limit, display_inline_comments):
517 517 vcs_diff = PullRequestModel().get_diff(
518 518 source_repo, source_ref_id, target_ref_id)
519 519
520 520 diff_processor = diffs.DiffProcessor(
521 521 vcs_diff, format='newdiff', diff_limit=diff_limit,
522 522 file_limit=file_limit, show_full_diff=c.fulldiff)
523 523
524 524 _parsed = diff_processor.prepare()
525 525
526 526 def _node_getter(commit):
527 527 def get_node(fname):
528 528 try:
529 529 return commit.get_node(fname)
530 530 except NodeDoesNotExistError:
531 531 return None
532 532
533 533 return get_node
534 534
535 535 diffset = codeblocks.DiffSet(
536 536 repo_name=c.repo_name,
537 537 source_repo_name=c.source_repo.repo_name,
538 538 source_node_getter=_node_getter(target_commit),
539 539 target_node_getter=_node_getter(source_commit),
540 540 comments=display_inline_comments
541 541 )
542 542 diffset = diffset.render_patchset(
543 543 _parsed, target_commit.raw_id, source_commit.raw_id)
544 544
545 545 return diffset
546 546
547 547 @LoginRequired()
548 548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
549 549 'repository.admin')
550 550 def show(self, repo_name, pull_request_id):
551 551 pull_request_id = safe_int(pull_request_id)
552 552 version = request.GET.get('version')
553 553 from_version = request.GET.get('from_version') or version
554 554 merge_checks = request.GET.get('merge_checks')
555 555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
556 556
557 557 (pull_request_latest,
558 558 pull_request_at_ver,
559 559 pull_request_display_obj,
560 560 at_version) = self._get_pr_version(
561 561 pull_request_id, version=version)
562 562 pr_closed = pull_request_latest.is_closed()
563 563
564 564 if pr_closed and (version or from_version):
565 565 # not allow to browse versions
566 566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
567 567 pull_request_id=pull_request_id))
568 568
569 569 versions = pull_request_display_obj.versions()
570 570
571 571 c.at_version = at_version
572 572 c.at_version_num = (at_version
573 573 if at_version and at_version != 'latest'
574 574 else None)
575 575 c.at_version_pos = ChangesetComment.get_index_from_version(
576 576 c.at_version_num, versions)
577 577
578 578 (prev_pull_request_latest,
579 579 prev_pull_request_at_ver,
580 580 prev_pull_request_display_obj,
581 581 prev_at_version) = self._get_pr_version(
582 582 pull_request_id, version=from_version)
583 583
584 584 c.from_version = prev_at_version
585 585 c.from_version_num = (prev_at_version
586 586 if prev_at_version and prev_at_version != 'latest'
587 587 else None)
588 588 c.from_version_pos = ChangesetComment.get_index_from_version(
589 589 c.from_version_num, versions)
590 590
591 591 # define if we're in COMPARE mode or VIEW at version mode
592 592 compare = at_version != prev_at_version
593 593
594 594 # pull_requests repo_name we opened it against
595 595 # ie. target_repo must match
596 596 if repo_name != pull_request_at_ver.target_repo.repo_name:
597 597 raise HTTPNotFound
598 598
599 599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
600 600 pull_request_at_ver)
601 601
602 602 c.pull_request = pull_request_display_obj
603 603 c.pull_request_latest = pull_request_latest
604 604
605 605 if compare or (at_version and not at_version == 'latest'):
606 606 c.allowed_to_change_status = False
607 607 c.allowed_to_update = False
608 608 c.allowed_to_merge = False
609 609 c.allowed_to_delete = False
610 610 c.allowed_to_comment = False
611 611 c.allowed_to_close = False
612 612 else:
613 613 can_change_status = PullRequestModel().check_user_change_status(
614 614 pull_request_at_ver, c.rhodecode_user)
615 615 c.allowed_to_change_status = can_change_status and not pr_closed
616 616
617 617 c.allowed_to_update = PullRequestModel().check_user_update(
618 618 pull_request_latest, c.rhodecode_user) and not pr_closed
619 619 c.allowed_to_merge = PullRequestModel().check_user_merge(
620 620 pull_request_latest, c.rhodecode_user) and not pr_closed
621 621 c.allowed_to_delete = PullRequestModel().check_user_delete(
622 622 pull_request_latest, c.rhodecode_user) and not pr_closed
623 623 c.allowed_to_comment = not pr_closed
624 624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
625 625
626 626 c.forbid_adding_reviewers = False
627 627 c.forbid_author_to_review = False
628 628 c.forbid_commit_author_to_review = False
629 629
630 630 if pull_request_latest.reviewer_data and \
631 631 'rules' in pull_request_latest.reviewer_data:
632 632 rules = pull_request_latest.reviewer_data['rules'] or {}
633 633 try:
634 634 c.forbid_adding_reviewers = rules.get(
635 635 'forbid_adding_reviewers')
636 636 c.forbid_author_to_review = rules.get(
637 637 'forbid_author_to_review')
638 638 c.forbid_commit_author_to_review = rules.get(
639 639 'forbid_commit_author_to_review')
640 640 except Exception:
641 641 pass
642 642
643 643 # check merge capabilities
644 644 _merge_check = MergeCheck.validate(
645 645 pull_request_latest, user=c.rhodecode_user)
646 646 c.pr_merge_errors = _merge_check.error_details
647 647 c.pr_merge_possible = not _merge_check.failed
648 648 c.pr_merge_message = _merge_check.merge_msg
649 649
650 650 c.pull_request_review_status = _merge_check.review_status
651 651 if merge_checks:
652 652 return render('/pullrequests/pullrequest_merge_checks.mako')
653 653
654 654 comments_model = CommentsModel()
655 655
656 656 # reviewers and statuses
657 657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
658 658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
659 659
660 660 # GENERAL COMMENTS with versions #
661 661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
662 662 q = q.order_by(ChangesetComment.comment_id.asc())
663 663 general_comments = q
664 664
665 665 # pick comments we want to render at current version
666 666 c.comment_versions = comments_model.aggregate_comments(
667 667 general_comments, versions, c.at_version_num)
668 668 c.comments = c.comment_versions[c.at_version_num]['until']
669 669
670 670 # INLINE COMMENTS with versions #
671 671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
672 672 q = q.order_by(ChangesetComment.comment_id.asc())
673 673 inline_comments = q
674 674
675 675 c.inline_versions = comments_model.aggregate_comments(
676 676 inline_comments, versions, c.at_version_num, inline=True)
677 677
678 678 # inject latest version
679 679 latest_ver = PullRequest.get_pr_display_object(
680 680 pull_request_latest, pull_request_latest)
681 681
682 682 c.versions = versions + [latest_ver]
683 683
684 684 # if we use version, then do not show later comments
685 685 # than current version
686 686 display_inline_comments = collections.defaultdict(
687 687 lambda: collections.defaultdict(list))
688 688 for co in inline_comments:
689 689 if c.at_version_num:
690 690 # pick comments that are at least UPTO given version, so we
691 691 # don't render comments for higher version
692 692 should_render = co.pull_request_version_id and \
693 693 co.pull_request_version_id <= c.at_version_num
694 694 else:
695 695 # showing all, for 'latest'
696 696 should_render = True
697 697
698 698 if should_render:
699 699 display_inline_comments[co.f_path][co.line_no].append(co)
700 700
701 701 # load diff data into template context, if we use compare mode then
702 702 # diff is calculated based on changes between versions of PR
703 703
704 704 source_repo = pull_request_at_ver.source_repo
705 705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
706 706
707 707 target_repo = pull_request_at_ver.target_repo
708 708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
709 709
710 710 if compare:
711 711 # in compare switch the diff base to latest commit from prev version
712 712 target_ref_id = prev_pull_request_display_obj.revisions[0]
713 713
714 714 # despite opening commits for bookmarks/branches/tags, we always
715 715 # convert this to rev to prevent changes after bookmark or branch change
716 716 c.source_ref_type = 'rev'
717 717 c.source_ref = source_ref_id
718 718
719 719 c.target_ref_type = 'rev'
720 720 c.target_ref = target_ref_id
721 721
722 722 c.source_repo = source_repo
723 723 c.target_repo = target_repo
724 724
725 725 # diff_limit is the old behavior, will cut off the whole diff
726 726 # if the limit is applied otherwise will just hide the
727 727 # big files from the front-end
728 728 diff_limit = self.cut_off_limit_diff
729 729 file_limit = self.cut_off_limit_file
730 730
731 731 c.commit_ranges = []
732 732 source_commit = EmptyCommit()
733 733 target_commit = EmptyCommit()
734 734 c.missing_requirements = False
735 735
736 736 source_scm = source_repo.scm_instance()
737 737 target_scm = target_repo.scm_instance()
738 738
739 739 # try first shadow repo, fallback to regular repo
740 740 try:
741 741 commits_source_repo = pull_request_latest.get_shadow_repo()
742 742 except Exception:
743 743 log.debug('Failed to get shadow repo', exc_info=True)
744 744 commits_source_repo = source_scm
745 745
746 746 c.commits_source_repo = commits_source_repo
747 747 commit_cache = {}
748 748 try:
749 749 pre_load = ["author", "branch", "date", "message"]
750 750 show_revs = pull_request_at_ver.revisions
751 751 for rev in show_revs:
752 752 comm = commits_source_repo.get_commit(
753 753 commit_id=rev, pre_load=pre_load)
754 754 c.commit_ranges.append(comm)
755 755 commit_cache[comm.raw_id] = comm
756 756
757 757 # Order here matters, we first need to get target, and then
758 758 # the source
759 759 target_commit = commits_source_repo.get_commit(
760 760 commit_id=safe_str(target_ref_id))
761 761
762 762 source_commit = commits_source_repo.get_commit(
763 763 commit_id=safe_str(source_ref_id))
764 764
765 765 except CommitDoesNotExistError:
766 766 log.warning(
767 767 'Failed to get commit from `{}` repo'.format(
768 768 commits_source_repo), exc_info=True)
769 769 except RepositoryRequirementError:
770 770 log.warning(
771 771 'Failed to get all required data from repo', exc_info=True)
772 772 c.missing_requirements = True
773 773
774 774 c.ancestor = None # set it to None, to hide it from PR view
775 775
776 776 try:
777 777 ancestor_id = source_scm.get_common_ancestor(
778 778 source_commit.raw_id, target_commit.raw_id, target_scm)
779 779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
780 780 except Exception:
781 781 c.ancestor_commit = None
782 782
783 783 c.statuses = source_repo.statuses(
784 784 [x.raw_id for x in c.commit_ranges])
785 785
786 786 # auto collapse if we have more than limit
787 787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
788 788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
789 789 c.compare_mode = compare
790 790
791 791 c.missing_commits = False
792 792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
793 793 or source_commit == target_commit):
794 794
795 795 c.missing_commits = True
796 796 else:
797 797
798 798 c.diffset = self._get_diffset(
799 799 commits_source_repo, source_ref_id, target_ref_id,
800 800 target_commit, source_commit,
801 801 diff_limit, file_limit, display_inline_comments)
802 802
803 803 c.limited_diff = c.diffset.limited_diff
804 804
805 805 # calculate removed files that are bound to comments
806 806 comment_deleted_files = [
807 807 fname for fname in display_inline_comments
808 808 if fname not in c.diffset.file_stats]
809 809
810 810 c.deleted_files_comments = collections.defaultdict(dict)
811 811 for fname, per_line_comments in display_inline_comments.items():
812 812 if fname in comment_deleted_files:
813 813 c.deleted_files_comments[fname]['stats'] = 0
814 814 c.deleted_files_comments[fname]['comments'] = list()
815 815 for lno, comments in per_line_comments.items():
816 816 c.deleted_files_comments[fname]['comments'].extend(
817 817 comments)
818 818
819 819 # this is a hack to properly display links, when creating PR, the
820 820 # compare view and others uses different notation, and
821 821 # compare_commits.mako renders links based on the target_repo.
822 822 # We need to swap that here to generate it properly on the html side
823 823 c.target_repo = c.source_repo
824 824
825 825 c.commit_statuses = ChangesetStatus.STATUSES
826 826
827 827 c.show_version_changes = not pr_closed
828 828 if c.show_version_changes:
829 829 cur_obj = pull_request_at_ver
830 830 prev_obj = prev_pull_request_at_ver
831 831
832 832 old_commit_ids = prev_obj.revisions
833 833 new_commit_ids = cur_obj.revisions
834 834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
835 835 old_commit_ids, new_commit_ids)
836 836 c.commit_changes_summary = commit_changes
837 837
838 838 # calculate the diff for commits between versions
839 839 c.commit_changes = []
840 840 mark = lambda cs, fw: list(
841 841 h.itertools.izip_longest([], cs, fillvalue=fw))
842 842 for c_type, raw_id in mark(commit_changes.added, 'a') \
843 843 + mark(commit_changes.removed, 'r') \
844 844 + mark(commit_changes.common, 'c'):
845 845
846 846 if raw_id in commit_cache:
847 847 commit = commit_cache[raw_id]
848 848 else:
849 849 try:
850 850 commit = commits_source_repo.get_commit(raw_id)
851 851 except CommitDoesNotExistError:
852 852 # in case we fail extracting still use "dummy" commit
853 853 # for display in commit diff
854 854 commit = h.AttributeDict(
855 855 {'raw_id': raw_id,
856 856 'message': 'EMPTY or MISSING COMMIT'})
857 857 c.commit_changes.append([c_type, commit])
858 858
859 859 # current user review statuses for each version
860 860 c.review_versions = {}
861 861 if c.rhodecode_user.user_id in allowed_reviewers:
862 862 for co in general_comments:
863 863 if co.author.user_id == c.rhodecode_user.user_id:
864 864 # each comment has a status change
865 865 status = co.status_change
866 866 if status:
867 867 _ver_pr = status[0].comment.pull_request_version_id
868 868 c.review_versions[_ver_pr] = status[0]
869 869
870 870 return render('/pullrequests/pullrequest_show.mako')
871 871
872 872 @LoginRequired()
873 873 @NotAnonymous()
874 874 @HasRepoPermissionAnyDecorator(
875 875 'repository.read', 'repository.write', 'repository.admin')
876 876 @auth.CSRFRequired()
877 877 @jsonify
878 878 def comment(self, repo_name, pull_request_id):
879 879 pull_request_id = safe_int(pull_request_id)
880 880 pull_request = PullRequest.get_or_404(pull_request_id)
881 881 if pull_request.is_closed():
882 882 log.debug('comment: forbidden because pull request is closed')
883 883 raise HTTPForbidden()
884 884
885 885 status = request.POST.get('changeset_status', None)
886 886 text = request.POST.get('text')
887 887 comment_type = request.POST.get('comment_type')
888 888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
889 889 close_pull_request = request.POST.get('close_pull_request')
890 890
891 891 # the logic here should work like following, if we submit close
892 892 # pr comment, use `close_pull_request_with_comment` function
893 893 # else handle regular comment logic
894 894 user = c.rhodecode_user
895 895 repo = c.rhodecode_db_repo
896 896
897 897 if close_pull_request:
898 898 # only owner or admin or person with write permissions
899 899 allowed_to_close = PullRequestModel().check_user_update(
900 900 pull_request, c.rhodecode_user)
901 901 if not allowed_to_close:
902 902 log.debug('comment: forbidden because not allowed to close '
903 903 'pull request %s', pull_request_id)
904 904 raise HTTPForbidden()
905 905 comment, status = PullRequestModel().close_pull_request_with_comment(
906 906 pull_request, user, repo, message=text)
907 907 Session().flush()
908 908 events.trigger(
909 909 events.PullRequestCommentEvent(pull_request, comment))
910 910
911 911 else:
912 912 # regular comment case, could be inline, or one with status.
913 913 # for that one we check also permissions
914 914
915 915 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 916 pull_request, c.rhodecode_user)
917 917
918 918 if status and allowed_to_change_status:
919 919 message = (_('Status change %(transition_icon)s %(status)s')
920 920 % {'transition_icon': '>',
921 921 'status': ChangesetStatus.get_status_lbl(status)})
922 922 text = text or message
923 923
924 924 comment = CommentsModel().create(
925 925 text=text,
926 926 repo=c.rhodecode_db_repo.repo_id,
927 927 user=c.rhodecode_user.user_id,
928 928 pull_request=pull_request_id,
929 929 f_path=request.POST.get('f_path'),
930 930 line_no=request.POST.get('line'),
931 931 status_change=(ChangesetStatus.get_status_lbl(status)
932 932 if status and allowed_to_change_status else None),
933 933 status_change_type=(status
934 934 if status and allowed_to_change_status else None),
935 935 comment_type=comment_type,
936 936 resolves_comment_id=resolves_comment_id
937 937 )
938 938
939 939 if allowed_to_change_status:
940 940 # calculate old status before we change it
941 941 old_calculated_status = pull_request.calculated_review_status()
942 942
943 943 # get status if set !
944 944 if status:
945 945 ChangesetStatusModel().set_status(
946 946 c.rhodecode_db_repo.repo_id,
947 947 status,
948 948 c.rhodecode_user.user_id,
949 949 comment,
950 950 pull_request=pull_request_id
951 951 )
952 952
953 953 Session().flush()
954 954 events.trigger(
955 955 events.PullRequestCommentEvent(pull_request, comment))
956 956
957 957 # we now calculate the status of pull request, and based on that
958 958 # calculation we set the commits status
959 959 calculated_status = pull_request.calculated_review_status()
960 960 if old_calculated_status != calculated_status:
961 961 PullRequestModel()._trigger_pull_request_hook(
962 962 pull_request, c.rhodecode_user, 'review_status_change')
963 963
964 964 Session().commit()
965 965
966 966 if not request.is_xhr:
967 967 raise HTTPFound(
968 968 h.route_path('pullrequest_show',
969 969 repo_name=repo_name,
970 970 pull_request_id=pull_request_id))
971 971
972 972 data = {
973 973 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
974 974 }
975 975 if comment:
976 976 c.co = comment
977 977 rendered_comment = render('changeset/changeset_comment_block.mako')
978 978 data.update(comment.get_dict())
979 979 data.update({'rendered_text': rendered_comment})
980 980
981 981 return data
982 982
983 983 @LoginRequired()
984 984 @NotAnonymous()
985 985 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
986 986 'repository.admin')
987 987 @auth.CSRFRequired()
988 988 @jsonify
989 989 def delete_comment(self, repo_name, comment_id):
990 return self._delete_comment(comment_id)
990 comment = ChangesetComment.get_or_404(safe_int(comment_id))
991 if not comment:
992 log.debug('Comment with id:%s not found, skipping', comment_id)
993 # comment already deleted in another call probably
994 return True
991 995
992 def _delete_comment(self, comment_id):
993 comment_id = safe_int(comment_id)
994 co = ChangesetComment.get_or_404(comment_id)
995 if co.pull_request.is_closed():
996 if comment.pull_request.is_closed():
996 997 # don't allow deleting comments on closed pull request
997 998 raise HTTPForbidden()
998 999
999 is_owner = co.author.user_id == c.rhodecode_user.user_id
1000 1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1001 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1002 old_calculated_status = co.pull_request.calculated_review_status()
1003 CommentsModel().delete(comment=co, user=c.rhodecode_user)
1001 super_admin = h.HasPermissionAny('hg.admin')()
1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1004 comment_repo_admin = is_repo_admin and is_repo_comment
1005
1006 if super_admin or comment_owner or comment_repo_admin:
1007 old_calculated_status = comment.pull_request.calculated_review_status()
1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1004 1009 Session().commit()
1005 calculated_status = co.pull_request.calculated_review_status()
1010 calculated_status = comment.pull_request.calculated_review_status()
1006 1011 if old_calculated_status != calculated_status:
1007 1012 PullRequestModel()._trigger_pull_request_hook(
1008 co.pull_request, c.rhodecode_user, 'review_status_change')
1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1009 1014 return True
1010 1015 else:
1011 1016 raise HTTPForbidden()
@@ -1,1093 +1,1094 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.lib import helpers as h
28 28 from rhodecode.model.changeset_status import ChangesetStatusModel
29 29 from rhodecode.model.db import (
30 30 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
31 31 from rhodecode.model.meta import Session
32 32 from rhodecode.model.pull_request import PullRequestModel
33 33 from rhodecode.model.user import UserModel
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(object):
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(h.route_path('repo_summary', 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_create_pr_form_with_raw_commit_id(self, backend):
57 57 repo = backend.repo
58 58
59 59 self.app.get(
60 60 url(controller='pullrequests', action='index',
61 61 repo_name=repo.repo_name,
62 62 commit=repo.get_commit().raw_id),
63 63 status=200)
64 64
65 65 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
66 66 def test_show(self, pr_util, pr_merge_enabled):
67 67 pull_request = pr_util.create_pull_request(
68 68 mergeable=pr_merge_enabled, enable_notifications=False)
69 69
70 70 response = self.app.get(url(
71 71 controller='pullrequests', action='show',
72 72 repo_name=pull_request.target_repo.scm_instance().name,
73 73 pull_request_id=str(pull_request.pull_request_id)))
74 74
75 75 for commit_id in pull_request.revisions:
76 76 response.mustcontain(commit_id)
77 77
78 78 assert pull_request.target_ref_parts.type in response
79 79 assert pull_request.target_ref_parts.name in response
80 80 target_clone_url = pull_request.target_repo.clone_url()
81 81 assert target_clone_url in response
82 82
83 83 assert 'class="pull-request-merge"' in response
84 84 assert (
85 85 'Server-side pull request merging is disabled.'
86 86 in response) != pr_merge_enabled
87 87
88 88 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
89 89 from rhodecode.tests.functional.test_login import login_url, logut_url
90 90 # Logout
91 91 response = self.app.post(
92 92 logut_url,
93 93 params={'csrf_token': csrf_token})
94 94 # Login as regular user
95 95 response = self.app.post(login_url,
96 96 {'username': TEST_USER_REGULAR_LOGIN,
97 97 'password': 'test12'})
98 98
99 99 pull_request = pr_util.create_pull_request(
100 100 author=TEST_USER_REGULAR_LOGIN)
101 101
102 102 response = self.app.get(url(
103 103 controller='pullrequests', action='show',
104 104 repo_name=pull_request.target_repo.scm_instance().name,
105 105 pull_request_id=str(pull_request.pull_request_id)))
106 106
107 107 response.mustcontain('Server-side pull request merging is disabled.')
108 108
109 109 assert_response = response.assert_response()
110 110 # for regular user without a merge permissions, we don't see it
111 111 assert_response.no_element_exists('#close-pull-request-action')
112 112
113 113 user_util.grant_user_permission_to_repo(
114 114 pull_request.target_repo,
115 115 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
116 116 'repository.write')
117 117 response = self.app.get(url(
118 118 controller='pullrequests', action='show',
119 119 repo_name=pull_request.target_repo.scm_instance().name,
120 120 pull_request_id=str(pull_request.pull_request_id)))
121 121
122 122 response.mustcontain('Server-side pull request merging is disabled.')
123 123
124 124 assert_response = response.assert_response()
125 125 # now regular user has a merge permissions, we have CLOSE button
126 126 assert_response.one_element_exists('#close-pull-request-action')
127 127
128 128 def test_show_invalid_commit_id(self, pr_util):
129 129 # Simulating invalid revisions which will cause a lookup error
130 130 pull_request = pr_util.create_pull_request()
131 131 pull_request.revisions = ['invalid']
132 132 Session().add(pull_request)
133 133 Session().commit()
134 134
135 135 response = self.app.get(url(
136 136 controller='pullrequests', action='show',
137 137 repo_name=pull_request.target_repo.scm_instance().name,
138 138 pull_request_id=str(pull_request.pull_request_id)))
139 139
140 140 for commit_id in pull_request.revisions:
141 141 response.mustcontain(commit_id)
142 142
143 143 def test_show_invalid_source_reference(self, pr_util):
144 144 pull_request = pr_util.create_pull_request()
145 145 pull_request.source_ref = 'branch:b:invalid'
146 146 Session().add(pull_request)
147 147 Session().commit()
148 148
149 149 self.app.get(url(
150 150 controller='pullrequests', action='show',
151 151 repo_name=pull_request.target_repo.scm_instance().name,
152 152 pull_request_id=str(pull_request.pull_request_id)))
153 153
154 154 def test_edit_title_description(self, pr_util, csrf_token):
155 155 pull_request = pr_util.create_pull_request()
156 156 pull_request_id = pull_request.pull_request_id
157 157
158 158 response = self.app.post(
159 159 url(controller='pullrequests', action='update',
160 160 repo_name=pull_request.target_repo.repo_name,
161 161 pull_request_id=str(pull_request_id)),
162 162 params={
163 163 'edit_pull_request': 'true',
164 164 '_method': 'put',
165 165 'title': 'New title',
166 166 'description': 'New description',
167 167 'csrf_token': csrf_token})
168 168
169 169 assert_session_flash(
170 170 response, u'Pull request title & description updated.',
171 171 category='success')
172 172
173 173 pull_request = PullRequest.get(pull_request_id)
174 174 assert pull_request.title == 'New title'
175 175 assert pull_request.description == 'New description'
176 176
177 177 def test_edit_title_description_closed(self, pr_util, csrf_token):
178 178 pull_request = pr_util.create_pull_request()
179 179 pull_request_id = pull_request.pull_request_id
180 180 pr_util.close()
181 181
182 182 response = self.app.post(
183 183 url(controller='pullrequests', action='update',
184 184 repo_name=pull_request.target_repo.repo_name,
185 185 pull_request_id=str(pull_request_id)),
186 186 params={
187 187 'edit_pull_request': 'true',
188 188 '_method': 'put',
189 189 'title': 'New title',
190 190 'description': 'New description',
191 191 'csrf_token': csrf_token})
192 192
193 193 assert_session_flash(
194 194 response, u'Cannot update closed pull requests.',
195 195 category='error')
196 196
197 197 def test_update_invalid_source_reference(self, pr_util, csrf_token):
198 198 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
199 199
200 200 pull_request = pr_util.create_pull_request()
201 201 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
202 202 Session().add(pull_request)
203 203 Session().commit()
204 204
205 205 pull_request_id = pull_request.pull_request_id
206 206
207 207 response = self.app.post(
208 208 url(controller='pullrequests', action='update',
209 209 repo_name=pull_request.target_repo.repo_name,
210 210 pull_request_id=str(pull_request_id)),
211 211 params={'update_commits': 'true', '_method': 'put',
212 212 'csrf_token': csrf_token})
213 213
214 214 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
215 215 UpdateFailureReason.MISSING_SOURCE_REF]
216 216 assert_session_flash(response, expected_msg, category='error')
217 217
218 218 def test_missing_target_reference(self, pr_util, csrf_token):
219 219 from rhodecode.lib.vcs.backends.base import MergeFailureReason
220 220 pull_request = pr_util.create_pull_request(
221 221 approved=True, mergeable=True)
222 222 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
223 223 Session().add(pull_request)
224 224 Session().commit()
225 225
226 226 pull_request_id = pull_request.pull_request_id
227 227 pull_request_url = url(
228 228 controller='pullrequests', action='show',
229 229 repo_name=pull_request.target_repo.repo_name,
230 230 pull_request_id=str(pull_request_id))
231 231
232 232 response = self.app.get(pull_request_url)
233 233
234 234 assertr = AssertResponse(response)
235 235 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
236 236 MergeFailureReason.MISSING_TARGET_REF]
237 237 assertr.element_contains(
238 238 'span[data-role="merge-message"]', str(expected_msg))
239 239
240 240 def test_comment_and_close_pull_request_custom_message_approved(
241 241 self, pr_util, csrf_token, xhr_header):
242 242
243 243 pull_request = pr_util.create_pull_request(approved=True)
244 244 pull_request_id = pull_request.pull_request_id
245 245 author = pull_request.user_id
246 246 repo = pull_request.target_repo.repo_id
247 247
248 248 self.app.post(
249 249 url(controller='pullrequests',
250 250 action='comment',
251 251 repo_name=pull_request.target_repo.scm_instance().name,
252 252 pull_request_id=str(pull_request_id)),
253 253 params={
254 254 'close_pull_request': '1',
255 255 'text': 'Closing a PR',
256 256 'csrf_token': csrf_token},
257 257 extra_environ=xhr_header,)
258 258
259 259 journal = UserLog.query()\
260 260 .filter(UserLog.user_id == author)\
261 261 .filter(UserLog.repository_id == repo) \
262 262 .order_by('user_log_id') \
263 263 .all()
264 264 assert journal[-1].action == 'repo.pull_request.close'
265 265
266 266 pull_request = PullRequest.get(pull_request_id)
267 267 assert pull_request.is_closed()
268 268
269 269 status = ChangesetStatusModel().get_status(
270 270 pull_request.source_repo, pull_request=pull_request)
271 271 assert status == ChangesetStatus.STATUS_APPROVED
272 272 comments = ChangesetComment().query() \
273 273 .filter(ChangesetComment.pull_request == pull_request) \
274 274 .order_by(ChangesetComment.comment_id.asc())\
275 275 .all()
276 276 assert comments[-1].text == 'Closing a PR'
277 277
278 278 def test_comment_force_close_pull_request_rejected(
279 279 self, pr_util, csrf_token, xhr_header):
280 280 pull_request = pr_util.create_pull_request()
281 281 pull_request_id = pull_request.pull_request_id
282 282 PullRequestModel().update_reviewers(
283 283 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
284 284 pull_request.author)
285 285 author = pull_request.user_id
286 286 repo = pull_request.target_repo.repo_id
287 287
288 288 self.app.post(
289 289 url(controller='pullrequests',
290 290 action='comment',
291 291 repo_name=pull_request.target_repo.scm_instance().name,
292 292 pull_request_id=str(pull_request_id)),
293 293 params={
294 294 'close_pull_request': '1',
295 295 'csrf_token': csrf_token},
296 296 extra_environ=xhr_header)
297 297
298 298 pull_request = PullRequest.get(pull_request_id)
299 299
300 300 journal = UserLog.query()\
301 301 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
302 302 .order_by('user_log_id') \
303 303 .all()
304 304 assert journal[-1].action == 'repo.pull_request.close'
305 305
306 306 # check only the latest status, not the review status
307 307 status = ChangesetStatusModel().get_status(
308 308 pull_request.source_repo, pull_request=pull_request)
309 309 assert status == ChangesetStatus.STATUS_REJECTED
310 310
311 311 def test_comment_and_close_pull_request(
312 312 self, pr_util, csrf_token, xhr_header):
313 313 pull_request = pr_util.create_pull_request()
314 314 pull_request_id = pull_request.pull_request_id
315 315
316 316 response = self.app.post(
317 317 url(controller='pullrequests',
318 318 action='comment',
319 319 repo_name=pull_request.target_repo.scm_instance().name,
320 320 pull_request_id=str(pull_request.pull_request_id)),
321 321 params={
322 322 'close_pull_request': 'true',
323 323 'csrf_token': csrf_token},
324 324 extra_environ=xhr_header)
325 325
326 326 assert response.json
327 327
328 328 pull_request = PullRequest.get(pull_request_id)
329 329 assert pull_request.is_closed()
330 330
331 331 # check only the latest status, not the review status
332 332 status = ChangesetStatusModel().get_status(
333 333 pull_request.source_repo, pull_request=pull_request)
334 334 assert status == ChangesetStatus.STATUS_REJECTED
335 335
336 336 def test_create_pull_request(self, backend, csrf_token):
337 337 commits = [
338 338 {'message': 'ancestor'},
339 339 {'message': 'change'},
340 340 {'message': 'change2'},
341 341 ]
342 342 commit_ids = backend.create_master_repo(commits)
343 343 target = backend.create_repo(heads=['ancestor'])
344 344 source = backend.create_repo(heads=['change2'])
345 345
346 346 response = self.app.post(
347 347 url(
348 348 controller='pullrequests',
349 349 action='create',
350 350 repo_name=source.repo_name
351 351 ),
352 352 [
353 353 ('source_repo', source.repo_name),
354 354 ('source_ref', 'branch:default:' + commit_ids['change2']),
355 355 ('target_repo', target.repo_name),
356 356 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
357 357 ('common_ancestor', commit_ids['ancestor']),
358 358 ('pullrequest_desc', 'Description'),
359 359 ('pullrequest_title', 'Title'),
360 360 ('__start__', 'review_members:sequence'),
361 361 ('__start__', 'reviewer:mapping'),
362 362 ('user_id', '1'),
363 363 ('__start__', 'reasons:sequence'),
364 364 ('reason', 'Some reason'),
365 365 ('__end__', 'reasons:sequence'),
366 366 ('mandatory', 'False'),
367 367 ('__end__', 'reviewer:mapping'),
368 368 ('__end__', 'review_members:sequence'),
369 369 ('__start__', 'revisions:sequence'),
370 370 ('revisions', commit_ids['change']),
371 371 ('revisions', commit_ids['change2']),
372 372 ('__end__', 'revisions:sequence'),
373 373 ('user', ''),
374 374 ('csrf_token', csrf_token),
375 375 ],
376 376 status=302)
377 377
378 378 location = response.headers['Location']
379 379 pull_request_id = location.rsplit('/', 1)[1]
380 380 assert pull_request_id != 'new'
381 381 pull_request = PullRequest.get(int(pull_request_id))
382 382
383 383 # check that we have now both revisions
384 384 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
385 385 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
386 386 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
387 387 assert pull_request.target_ref == expected_target_ref
388 388
389 389 def test_reviewer_notifications(self, backend, csrf_token):
390 390 # We have to use the app.post for this test so it will create the
391 391 # notifications properly with the new PR
392 392 commits = [
393 393 {'message': 'ancestor',
394 394 'added': [FileNode('file_A', content='content_of_ancestor')]},
395 395 {'message': 'change',
396 396 'added': [FileNode('file_a', content='content_of_change')]},
397 397 {'message': 'change-child'},
398 398 {'message': 'ancestor-child', 'parents': ['ancestor'],
399 399 'added': [
400 400 FileNode('file_B', content='content_of_ancestor_child')]},
401 401 {'message': 'ancestor-child-2'},
402 402 ]
403 403 commit_ids = backend.create_master_repo(commits)
404 404 target = backend.create_repo(heads=['ancestor-child'])
405 405 source = backend.create_repo(heads=['change'])
406 406
407 407 response = self.app.post(
408 408 url(
409 409 controller='pullrequests',
410 410 action='create',
411 411 repo_name=source.repo_name
412 412 ),
413 413 [
414 414 ('source_repo', source.repo_name),
415 415 ('source_ref', 'branch:default:' + commit_ids['change']),
416 416 ('target_repo', target.repo_name),
417 417 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
418 418 ('common_ancestor', commit_ids['ancestor']),
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 ('mandatory', 'False'),
428 428 ('__end__', 'reviewer:mapping'),
429 429 ('__end__', 'review_members:sequence'),
430 430 ('__start__', 'revisions:sequence'),
431 431 ('revisions', commit_ids['change']),
432 432 ('__end__', 'revisions:sequence'),
433 433 ('user', ''),
434 434 ('csrf_token', csrf_token),
435 435 ],
436 436 status=302)
437 437
438 438 location = response.headers['Location']
439 439
440 440 pull_request_id = location.rsplit('/', 1)[1]
441 441 assert pull_request_id != 'new'
442 442 pull_request = PullRequest.get(int(pull_request_id))
443 443
444 444 # Check that a notification was made
445 445 notifications = Notification.query()\
446 446 .filter(Notification.created_by == pull_request.author.user_id,
447 447 Notification.type_ == Notification.TYPE_PULL_REQUEST,
448 448 Notification.subject.contains(
449 449 "wants you to review pull request #%s" % pull_request_id))
450 450 assert len(notifications.all()) == 1
451 451
452 452 # Change reviewers and check that a notification was made
453 453 PullRequestModel().update_reviewers(
454 454 pull_request.pull_request_id, [(1, [], False)],
455 455 pull_request.author)
456 456 assert len(notifications.all()) == 2
457 457
458 458 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
459 459 csrf_token):
460 460 commits = [
461 461 {'message': 'ancestor',
462 462 'added': [FileNode('file_A', content='content_of_ancestor')]},
463 463 {'message': 'change',
464 464 'added': [FileNode('file_a', content='content_of_change')]},
465 465 {'message': 'change-child'},
466 466 {'message': 'ancestor-child', 'parents': ['ancestor'],
467 467 'added': [
468 468 FileNode('file_B', content='content_of_ancestor_child')]},
469 469 {'message': 'ancestor-child-2'},
470 470 ]
471 471 commit_ids = backend.create_master_repo(commits)
472 472 target = backend.create_repo(heads=['ancestor-child'])
473 473 source = backend.create_repo(heads=['change'])
474 474
475 475 response = self.app.post(
476 476 url(
477 477 controller='pullrequests',
478 478 action='create',
479 479 repo_name=source.repo_name
480 480 ),
481 481 [
482 482 ('source_repo', source.repo_name),
483 483 ('source_ref', 'branch:default:' + commit_ids['change']),
484 484 ('target_repo', target.repo_name),
485 485 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
486 486 ('common_ancestor', commit_ids['ancestor']),
487 487 ('pullrequest_desc', 'Description'),
488 488 ('pullrequest_title', 'Title'),
489 489 ('__start__', 'review_members:sequence'),
490 490 ('__start__', 'reviewer:mapping'),
491 491 ('user_id', '1'),
492 492 ('__start__', 'reasons:sequence'),
493 493 ('reason', 'Some reason'),
494 494 ('__end__', 'reasons:sequence'),
495 495 ('mandatory', 'False'),
496 496 ('__end__', 'reviewer:mapping'),
497 497 ('__end__', 'review_members:sequence'),
498 498 ('__start__', 'revisions:sequence'),
499 499 ('revisions', commit_ids['change']),
500 500 ('__end__', 'revisions:sequence'),
501 501 ('user', ''),
502 502 ('csrf_token', csrf_token),
503 503 ],
504 504 status=302)
505 505
506 506 location = response.headers['Location']
507 507
508 508 pull_request_id = location.rsplit('/', 1)[1]
509 509 assert pull_request_id != 'new'
510 510 pull_request = PullRequest.get(int(pull_request_id))
511 511
512 512 # target_ref has to point to the ancestor's commit_id in order to
513 513 # show the correct diff
514 514 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
515 515 assert pull_request.target_ref == expected_target_ref
516 516
517 517 # Check generated diff contents
518 518 response = response.follow()
519 519 assert 'content_of_ancestor' not in response.body
520 520 assert 'content_of_ancestor-child' not in response.body
521 521 assert 'content_of_change' in response.body
522 522
523 523 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
524 524 # Clear any previous calls to rcextensions
525 525 rhodecode.EXTENSIONS.calls.clear()
526 526
527 527 pull_request = pr_util.create_pull_request(
528 528 approved=True, mergeable=True)
529 529 pull_request_id = pull_request.pull_request_id
530 530 repo_name = pull_request.target_repo.scm_instance().name,
531 531
532 532 response = self.app.post(
533 533 url(controller='pullrequests',
534 534 action='merge',
535 535 repo_name=str(repo_name[0]),
536 536 pull_request_id=str(pull_request_id)),
537 537 params={'csrf_token': csrf_token}).follow()
538 538
539 539 pull_request = PullRequest.get(pull_request_id)
540 540
541 541 assert response.status_int == 200
542 542 assert pull_request.is_closed()
543 543 assert_pull_request_status(
544 544 pull_request, ChangesetStatus.STATUS_APPROVED)
545 545
546 546 # Check the relevant log entries were added
547 547 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
548 548 actions = [log.action for log in user_logs]
549 549 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
550 550 expected_actions = [
551 551 u'repo.pull_request.close',
552 552 u'repo.pull_request.merge',
553 553 u'repo.pull_request.comment.create'
554 554 ]
555 555 assert actions == expected_actions
556 556
557 557 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
558 558 actions = [log for log in user_logs]
559 559 assert actions[-1].action == 'user.push'
560 560 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
561 561
562 562 # Check post_push rcextension was really executed
563 563 push_calls = rhodecode.EXTENSIONS.calls['post_push']
564 564 assert len(push_calls) == 1
565 565 unused_last_call_args, last_call_kwargs = push_calls[0]
566 566 assert last_call_kwargs['action'] == 'push'
567 567 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
568 568
569 569 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
570 570 pull_request = pr_util.create_pull_request(mergeable=False)
571 571 pull_request_id = pull_request.pull_request_id
572 572 pull_request = PullRequest.get(pull_request_id)
573 573
574 574 response = self.app.post(
575 575 url(controller='pullrequests',
576 576 action='merge',
577 577 repo_name=pull_request.target_repo.scm_instance().name,
578 578 pull_request_id=str(pull_request.pull_request_id)),
579 579 params={'csrf_token': csrf_token}).follow()
580 580
581 581 assert response.status_int == 200
582 582 response.mustcontain(
583 583 'Merge is not currently possible because of below failed checks.')
584 584 response.mustcontain('Server-side pull request merging is disabled.')
585 585
586 586 @pytest.mark.skip_backends('svn')
587 587 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
588 588 pull_request = pr_util.create_pull_request(mergeable=True)
589 589 pull_request_id = pull_request.pull_request_id
590 590 repo_name = pull_request.target_repo.scm_instance().name,
591 591
592 592 response = self.app.post(
593 593 url(controller='pullrequests',
594 594 action='merge',
595 595 repo_name=str(repo_name[0]),
596 596 pull_request_id=str(pull_request_id)),
597 597 params={'csrf_token': csrf_token}).follow()
598 598
599 599 assert response.status_int == 200
600 600
601 601 response.mustcontain(
602 602 'Merge is not currently possible because of below failed checks.')
603 603 response.mustcontain('Pull request reviewer approval is pending.')
604 604
605 605 def test_update_source_revision(self, backend, csrf_token):
606 606 commits = [
607 607 {'message': 'ancestor'},
608 608 {'message': 'change'},
609 609 {'message': 'change-2'},
610 610 ]
611 611 commit_ids = backend.create_master_repo(commits)
612 612 target = backend.create_repo(heads=['ancestor'])
613 613 source = backend.create_repo(heads=['change'])
614 614
615 615 # create pr from a in source to A in target
616 616 pull_request = PullRequest()
617 617 pull_request.source_repo = source
618 618 # TODO: johbo: Make sure that we write the source ref this way!
619 619 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
620 620 branch=backend.default_branch_name, commit_id=commit_ids['change'])
621 621 pull_request.target_repo = target
622 622
623 623 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
624 624 branch=backend.default_branch_name,
625 625 commit_id=commit_ids['ancestor'])
626 626 pull_request.revisions = [commit_ids['change']]
627 627 pull_request.title = u"Test"
628 628 pull_request.description = u"Description"
629 629 pull_request.author = UserModel().get_by_username(
630 630 TEST_USER_ADMIN_LOGIN)
631 631 Session().add(pull_request)
632 632 Session().commit()
633 633 pull_request_id = pull_request.pull_request_id
634 634
635 635 # source has ancestor - change - change-2
636 636 backend.pull_heads(source, heads=['change-2'])
637 637
638 638 # update PR
639 639 self.app.post(
640 640 url(controller='pullrequests', action='update',
641 641 repo_name=target.repo_name,
642 642 pull_request_id=str(pull_request_id)),
643 643 params={'update_commits': 'true', '_method': 'put',
644 644 'csrf_token': csrf_token})
645 645
646 646 # check that we have now both revisions
647 647 pull_request = PullRequest.get(pull_request_id)
648 648 assert pull_request.revisions == [
649 649 commit_ids['change-2'], commit_ids['change']]
650 650
651 651 # TODO: johbo: this should be a test on its own
652 652 response = self.app.get(url(
653 653 controller='pullrequests', action='index',
654 654 repo_name=target.repo_name))
655 655 assert response.status_int == 200
656 656 assert 'Pull request updated to' in response.body
657 657 assert 'with 1 added, 0 removed commits.' in response.body
658 658
659 659 def test_update_target_revision(self, backend, csrf_token):
660 660 commits = [
661 661 {'message': 'ancestor'},
662 662 {'message': 'change'},
663 663 {'message': 'ancestor-new', 'parents': ['ancestor']},
664 664 {'message': 'change-rebased'},
665 665 ]
666 666 commit_ids = backend.create_master_repo(commits)
667 667 target = backend.create_repo(heads=['ancestor'])
668 668 source = backend.create_repo(heads=['change'])
669 669
670 670 # create pr from a in source to A in target
671 671 pull_request = PullRequest()
672 672 pull_request.source_repo = source
673 673 # TODO: johbo: Make sure that we write the source ref this way!
674 674 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
675 675 branch=backend.default_branch_name, commit_id=commit_ids['change'])
676 676 pull_request.target_repo = target
677 677 # TODO: johbo: Target ref should be branch based, since tip can jump
678 678 # from branch to branch
679 679 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
680 680 branch=backend.default_branch_name,
681 681 commit_id=commit_ids['ancestor'])
682 682 pull_request.revisions = [commit_ids['change']]
683 683 pull_request.title = u"Test"
684 684 pull_request.description = u"Description"
685 685 pull_request.author = UserModel().get_by_username(
686 686 TEST_USER_ADMIN_LOGIN)
687 687 Session().add(pull_request)
688 688 Session().commit()
689 689 pull_request_id = pull_request.pull_request_id
690 690
691 691 # target has ancestor - ancestor-new
692 692 # source has ancestor - ancestor-new - change-rebased
693 693 backend.pull_heads(target, heads=['ancestor-new'])
694 694 backend.pull_heads(source, heads=['change-rebased'])
695 695
696 696 # update PR
697 697 self.app.post(
698 698 url(controller='pullrequests', action='update',
699 699 repo_name=target.repo_name,
700 700 pull_request_id=str(pull_request_id)),
701 701 params={'update_commits': 'true', '_method': 'put',
702 702 'csrf_token': csrf_token},
703 703 status=200)
704 704
705 705 # check that we have now both revisions
706 706 pull_request = PullRequest.get(pull_request_id)
707 707 assert pull_request.revisions == [commit_ids['change-rebased']]
708 708 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
709 709 branch=backend.default_branch_name,
710 710 commit_id=commit_ids['ancestor-new'])
711 711
712 712 # TODO: johbo: This should be a test on its own
713 713 response = self.app.get(url(
714 714 controller='pullrequests', action='index',
715 715 repo_name=target.repo_name))
716 716 assert response.status_int == 200
717 717 assert 'Pull request updated to' in response.body
718 718 assert 'with 1 added, 1 removed commits.' in response.body
719 719
720 720 def test_update_of_ancestor_reference(self, backend, csrf_token):
721 721 commits = [
722 722 {'message': 'ancestor'},
723 723 {'message': 'change'},
724 724 {'message': 'change-2'},
725 725 {'message': 'ancestor-new', 'parents': ['ancestor']},
726 726 {'message': 'change-rebased'},
727 727 ]
728 728 commit_ids = backend.create_master_repo(commits)
729 729 target = backend.create_repo(heads=['ancestor'])
730 730 source = backend.create_repo(heads=['change'])
731 731
732 732 # create pr from a in source to A in target
733 733 pull_request = PullRequest()
734 734 pull_request.source_repo = source
735 735 # TODO: johbo: Make sure that we write the source ref this way!
736 736 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
737 737 branch=backend.default_branch_name,
738 738 commit_id=commit_ids['change'])
739 739 pull_request.target_repo = target
740 740 # TODO: johbo: Target ref should be branch based, since tip can jump
741 741 # from branch to branch
742 742 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
743 743 branch=backend.default_branch_name,
744 744 commit_id=commit_ids['ancestor'])
745 745 pull_request.revisions = [commit_ids['change']]
746 746 pull_request.title = u"Test"
747 747 pull_request.description = u"Description"
748 748 pull_request.author = UserModel().get_by_username(
749 749 TEST_USER_ADMIN_LOGIN)
750 750 Session().add(pull_request)
751 751 Session().commit()
752 752 pull_request_id = pull_request.pull_request_id
753 753
754 754 # target has ancestor - ancestor-new
755 755 # source has ancestor - ancestor-new - change-rebased
756 756 backend.pull_heads(target, heads=['ancestor-new'])
757 757 backend.pull_heads(source, heads=['change-rebased'])
758 758
759 759 # update PR
760 760 self.app.post(
761 761 url(controller='pullrequests', action='update',
762 762 repo_name=target.repo_name,
763 763 pull_request_id=str(pull_request_id)),
764 764 params={'update_commits': 'true', '_method': 'put',
765 765 'csrf_token': csrf_token},
766 766 status=200)
767 767
768 768 # Expect the target reference to be updated correctly
769 769 pull_request = PullRequest.get(pull_request_id)
770 770 assert pull_request.revisions == [commit_ids['change-rebased']]
771 771 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
772 772 branch=backend.default_branch_name,
773 773 commit_id=commit_ids['ancestor-new'])
774 774 assert pull_request.target_ref == expected_target_ref
775 775
776 776 def test_remove_pull_request_branch(self, backend_git, csrf_token):
777 777 branch_name = 'development'
778 778 commits = [
779 779 {'message': 'initial-commit'},
780 780 {'message': 'old-feature'},
781 781 {'message': 'new-feature', 'branch': branch_name},
782 782 ]
783 783 repo = backend_git.create_repo(commits)
784 784 commit_ids = backend_git.commit_ids
785 785
786 786 pull_request = PullRequest()
787 787 pull_request.source_repo = repo
788 788 pull_request.target_repo = repo
789 789 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
790 790 branch=branch_name, commit_id=commit_ids['new-feature'])
791 791 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
792 792 branch=backend_git.default_branch_name,
793 793 commit_id=commit_ids['old-feature'])
794 794 pull_request.revisions = [commit_ids['new-feature']]
795 795 pull_request.title = u"Test"
796 796 pull_request.description = u"Description"
797 797 pull_request.author = UserModel().get_by_username(
798 798 TEST_USER_ADMIN_LOGIN)
799 799 Session().add(pull_request)
800 800 Session().commit()
801 801
802 802 vcs = repo.scm_instance()
803 803 vcs.remove_ref('refs/heads/{}'.format(branch_name))
804 804
805 805 response = self.app.get(url(
806 806 controller='pullrequests', action='show',
807 807 repo_name=repo.repo_name,
808 808 pull_request_id=str(pull_request.pull_request_id)))
809 809
810 810 assert response.status_int == 200
811 811 assert_response = AssertResponse(response)
812 812 assert_response.element_contains(
813 813 '#changeset_compare_view_content .alert strong',
814 814 'Missing commits')
815 815 assert_response.element_contains(
816 816 '#changeset_compare_view_content .alert',
817 817 'This pull request cannot be displayed, because one or more'
818 818 ' commits no longer exist in the source repository.')
819 819
820 820 def test_strip_commits_from_pull_request(
821 821 self, backend, pr_util, csrf_token):
822 822 commits = [
823 823 {'message': 'initial-commit'},
824 824 {'message': 'old-feature'},
825 825 {'message': 'new-feature', 'parents': ['initial-commit']},
826 826 ]
827 827 pull_request = pr_util.create_pull_request(
828 828 commits, target_head='initial-commit', source_head='new-feature',
829 829 revisions=['new-feature'])
830 830
831 831 vcs = pr_util.source_repository.scm_instance()
832 832 if backend.alias == 'git':
833 833 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
834 834 else:
835 835 vcs.strip(pr_util.commit_ids['new-feature'])
836 836
837 837 response = self.app.get(url(
838 838 controller='pullrequests', action='show',
839 839 repo_name=pr_util.target_repository.repo_name,
840 840 pull_request_id=str(pull_request.pull_request_id)))
841 841
842 842 assert response.status_int == 200
843 843 assert_response = AssertResponse(response)
844 844 assert_response.element_contains(
845 845 '#changeset_compare_view_content .alert strong',
846 846 'Missing commits')
847 847 assert_response.element_contains(
848 848 '#changeset_compare_view_content .alert',
849 849 'This pull request cannot be displayed, because one or more'
850 850 ' commits no longer exist in the source repository.')
851 851 assert_response.element_contains(
852 852 '#update_commits',
853 853 'Update commits')
854 854
855 855 def test_strip_commits_and_update(
856 856 self, backend, pr_util, csrf_token):
857 857 commits = [
858 858 {'message': 'initial-commit'},
859 859 {'message': 'old-feature'},
860 860 {'message': 'new-feature', 'parents': ['old-feature']},
861 861 ]
862 862 pull_request = pr_util.create_pull_request(
863 863 commits, target_head='old-feature', source_head='new-feature',
864 864 revisions=['new-feature'], mergeable=True)
865 865
866 866 vcs = pr_util.source_repository.scm_instance()
867 867 if backend.alias == 'git':
868 868 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
869 869 else:
870 870 vcs.strip(pr_util.commit_ids['new-feature'])
871 871
872 872 response = self.app.post(
873 873 url(controller='pullrequests', action='update',
874 874 repo_name=pull_request.target_repo.repo_name,
875 875 pull_request_id=str(pull_request.pull_request_id)),
876 876 params={'update_commits': 'true', '_method': 'put',
877 877 'csrf_token': csrf_token})
878 878
879 879 assert response.status_int == 200
880 880 assert response.body == 'true'
881 881
882 882 # Make sure that after update, it won't raise 500 errors
883 883 response = self.app.get(url(
884 884 controller='pullrequests', action='show',
885 885 repo_name=pr_util.target_repository.repo_name,
886 886 pull_request_id=str(pull_request.pull_request_id)))
887 887
888 888 assert response.status_int == 200
889 889 assert_response = AssertResponse(response)
890 890 assert_response.element_contains(
891 891 '#changeset_compare_view_content .alert strong',
892 892 'Missing commits')
893 893
894 894 def test_branch_is_a_link(self, pr_util):
895 895 pull_request = pr_util.create_pull_request()
896 896 pull_request.source_ref = 'branch:origin:1234567890abcdef'
897 897 pull_request.target_ref = 'branch:target:abcdef1234567890'
898 898 Session().add(pull_request)
899 899 Session().commit()
900 900
901 901 response = self.app.get(url(
902 902 controller='pullrequests', action='show',
903 903 repo_name=pull_request.target_repo.scm_instance().name,
904 904 pull_request_id=str(pull_request.pull_request_id)))
905 905 assert response.status_int == 200
906 906 assert_response = AssertResponse(response)
907 907
908 908 origin = assert_response.get_element('.pr-origininfo .tag')
909 909 origin_children = origin.getchildren()
910 910 assert len(origin_children) == 1
911 911 target = assert_response.get_element('.pr-targetinfo .tag')
912 912 target_children = target.getchildren()
913 913 assert len(target_children) == 1
914 914
915 915 expected_origin_link = url(
916 916 'changelog_home',
917 917 repo_name=pull_request.source_repo.scm_instance().name,
918 918 branch='origin')
919 919 expected_target_link = url(
920 920 'changelog_home',
921 921 repo_name=pull_request.target_repo.scm_instance().name,
922 922 branch='target')
923 923 assert origin_children[0].attrib['href'] == expected_origin_link
924 924 assert origin_children[0].text == 'branch: origin'
925 925 assert target_children[0].attrib['href'] == expected_target_link
926 926 assert target_children[0].text == 'branch: target'
927 927
928 928 def test_bookmark_is_not_a_link(self, pr_util):
929 929 pull_request = pr_util.create_pull_request()
930 930 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
931 931 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
932 932 Session().add(pull_request)
933 933 Session().commit()
934 934
935 935 response = self.app.get(url(
936 936 controller='pullrequests', action='show',
937 937 repo_name=pull_request.target_repo.scm_instance().name,
938 938 pull_request_id=str(pull_request.pull_request_id)))
939 939 assert response.status_int == 200
940 940 assert_response = AssertResponse(response)
941 941
942 942 origin = assert_response.get_element('.pr-origininfo .tag')
943 943 assert origin.text.strip() == 'bookmark: origin'
944 944 assert origin.getchildren() == []
945 945
946 946 target = assert_response.get_element('.pr-targetinfo .tag')
947 947 assert target.text.strip() == 'bookmark: target'
948 948 assert target.getchildren() == []
949 949
950 950 def test_tag_is_not_a_link(self, pr_util):
951 951 pull_request = pr_util.create_pull_request()
952 952 pull_request.source_ref = 'tag:origin:1234567890abcdef'
953 953 pull_request.target_ref = 'tag:target:abcdef1234567890'
954 954 Session().add(pull_request)
955 955 Session().commit()
956 956
957 957 response = self.app.get(url(
958 958 controller='pullrequests', action='show',
959 959 repo_name=pull_request.target_repo.scm_instance().name,
960 960 pull_request_id=str(pull_request.pull_request_id)))
961 961 assert response.status_int == 200
962 962 assert_response = AssertResponse(response)
963 963
964 964 origin = assert_response.get_element('.pr-origininfo .tag')
965 965 assert origin.text.strip() == 'tag: origin'
966 966 assert origin.getchildren() == []
967 967
968 968 target = assert_response.get_element('.pr-targetinfo .tag')
969 969 assert target.text.strip() == 'tag: target'
970 970 assert target.getchildren() == []
971 971
972 972 @pytest.mark.parametrize('mergeable', [True, False])
973 973 def test_shadow_repository_link(
974 974 self, mergeable, pr_util, http_host_only_stub):
975 975 """
976 976 Check that the pull request summary page displays a link to the shadow
977 977 repository if the pull request is mergeable. If it is not mergeable
978 978 the link should not be displayed.
979 979 """
980 980 pull_request = pr_util.create_pull_request(
981 981 mergeable=mergeable, enable_notifications=False)
982 982 target_repo = pull_request.target_repo.scm_instance()
983 983 pr_id = pull_request.pull_request_id
984 984 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
985 985 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
986 986
987 987 response = self.app.get(url(
988 988 controller='pullrequests', action='show',
989 989 repo_name=target_repo.name,
990 990 pull_request_id=str(pr_id)))
991 991
992 992 assertr = AssertResponse(response)
993 993 if mergeable:
994 994 assertr.element_value_contains(
995 995 'div.pr-mergeinfo input', shadow_url)
996 996 assertr.element_value_contains(
997 997 'div.pr-mergeinfo input', 'pr-merge')
998 998 else:
999 999 assertr.no_element_exists('div.pr-mergeinfo')
1000 1000
1001 1001
1002 1002 @pytest.mark.usefixtures('app')
1003 1003 @pytest.mark.backends("git", "hg")
1004 1004 class TestPullrequestsControllerDelete(object):
1005 1005 def test_pull_request_delete_button_permissions_admin(
1006 1006 self, autologin_user, user_admin, pr_util):
1007 1007 pull_request = pr_util.create_pull_request(
1008 1008 author=user_admin.username, enable_notifications=False)
1009 1009
1010 1010 response = self.app.get(url(
1011 1011 controller='pullrequests', action='show',
1012 1012 repo_name=pull_request.target_repo.scm_instance().name,
1013 1013 pull_request_id=str(pull_request.pull_request_id)))
1014 1014
1015 1015 response.mustcontain('id="delete_pullrequest"')
1016 1016 response.mustcontain('Confirm to delete this pull request')
1017 1017
1018 1018 def test_pull_request_delete_button_permissions_owner(
1019 1019 self, autologin_regular_user, user_regular, pr_util):
1020 1020 pull_request = pr_util.create_pull_request(
1021 1021 author=user_regular.username, enable_notifications=False)
1022 1022
1023 1023 response = self.app.get(url(
1024 1024 controller='pullrequests', action='show',
1025 1025 repo_name=pull_request.target_repo.scm_instance().name,
1026 1026 pull_request_id=str(pull_request.pull_request_id)))
1027 1027
1028 1028 response.mustcontain('id="delete_pullrequest"')
1029 1029 response.mustcontain('Confirm to delete this pull request')
1030 1030
1031 1031 def test_pull_request_delete_button_permissions_forbidden(
1032 1032 self, autologin_regular_user, user_regular, user_admin, pr_util):
1033 1033 pull_request = pr_util.create_pull_request(
1034 1034 author=user_admin.username, enable_notifications=False)
1035 1035
1036 1036 response = self.app.get(url(
1037 1037 controller='pullrequests', action='show',
1038 1038 repo_name=pull_request.target_repo.scm_instance().name,
1039 1039 pull_request_id=str(pull_request.pull_request_id)))
1040 1040 response.mustcontain(no=['id="delete_pullrequest"'])
1041 1041 response.mustcontain(no=['Confirm to delete this pull request'])
1042 1042
1043 1043 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1044 1044 self, autologin_regular_user, user_regular, user_admin, pr_util,
1045 1045 user_util):
1046 1046
1047 1047 pull_request = pr_util.create_pull_request(
1048 1048 author=user_admin.username, enable_notifications=False)
1049 1049
1050 1050 user_util.grant_user_permission_to_repo(
1051 1051 pull_request.target_repo, user_regular,
1052 1052 'repository.write')
1053 1053
1054 1054 response = self.app.get(url(
1055 1055 controller='pullrequests', action='show',
1056 1056 repo_name=pull_request.target_repo.scm_instance().name,
1057 1057 pull_request_id=str(pull_request.pull_request_id)))
1058 1058
1059 1059 response.mustcontain('id="open_edit_pullrequest"')
1060 1060 response.mustcontain('id="delete_pullrequest"')
1061 1061 response.mustcontain(no=['Confirm to delete this pull request'])
1062 1062
1063 def test_delete_comment_returns_404_if_comment_does_not_exist(
1064 self, autologin_user, pr_util, user_admin):
1065
1066 pull_request = pr_util.create_pull_request(
1067 author=user_admin.username, enable_notifications=False)
1068
1069 self.app.get(url(
1070 controller='pullrequests', action='delete_comment',
1071 repo_name=pull_request.target_repo.scm_instance().name,
1072 comment_id=1024404), status=404)
1073
1063 1074
1064 1075 def assert_pull_request_status(pull_request, expected_status):
1065 1076 status = ChangesetStatusModel().calculated_review_status(
1066 1077 pull_request=pull_request)
1067 1078 assert status == expected_status
1068 1079
1069 1080
1070 1081 @pytest.mark.parametrize('action', ['index', 'create'])
1071 1082 @pytest.mark.usefixtures("autologin_user")
1072 1083 def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action):
1073 1084 response = app.get(url(
1074 1085 controller='pullrequests', action=action,
1075 1086 repo_name=backend_svn.repo_name))
1076 1087 assert response.status_int == 302
1077 1088
1078 1089 # Not allowed, redirect to the summary
1079 1090 redirected = response.follow()
1080 1091 summary_url = h.route_path('repo_summary', repo_name=backend_svn.repo_name)
1081 1092
1082 1093 # URL adds leading slash and path doesn't have it
1083 1094 assert redirected.request.path == summary_url
1084
1085
1086 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1087 # TODO: johbo: Global import not possible because models.forms blows up
1088 from rhodecode.controllers.pullrequests import PullrequestsController
1089 controller = PullrequestsController()
1090 patcher = mock.patch(
1091 'rhodecode.model.db.BaseModel.get', return_value=None)
1092 with pytest.raises(HTTPNotFound), patcher:
1093 controller._delete_comment(1)
General Comments 0
You need to be logged in to leave comments. Login now