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