##// END OF EJS Templates
security: use 404 instead of 403 in case missing permissions for comment deletion....
ergo -
r1826:76aa3640 default
parent child Browse files
Show More
@@ -1,488 +1,490 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, safe_int
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_or_404(safe_int(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 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
440 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
441 super_admin = h.HasPermissionAny('hg.admin')()
441 super_admin = h.HasPermissionAny('hg.admin')()
442 comment_owner = (comment.author.user_id == c.rhodecode_user.user_id)
442 comment_owner = (comment.author.user_id == c.rhodecode_user.user_id)
443 is_repo_comment = comment.repo.repo_name == c.repo_name
443 is_repo_comment = comment.repo.repo_name == c.repo_name
444 comment_repo_admin = is_repo_admin and is_repo_comment
444 comment_repo_admin = is_repo_admin and is_repo_comment
445
445
446 if super_admin or comment_owner or comment_repo_admin:
446 if super_admin or comment_owner or comment_repo_admin:
447 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
447 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
448 Session().commit()
448 Session().commit()
449 return True
449 return True
450 else:
450 else:
451 raise HTTPForbidden()
451 log.warning('No permissions for user %s to delete comment_id: %s',
452 c.rhodecode_user, comment_id)
453 raise HTTPNotFound()
452
454
453 @LoginRequired()
455 @LoginRequired()
454 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
455 'repository.admin')
457 'repository.admin')
456 @jsonify
458 @jsonify
457 def changeset_info(self, repo_name, revision):
459 def changeset_info(self, repo_name, revision):
458 if request.is_xhr:
460 if request.is_xhr:
459 try:
461 try:
460 return c.rhodecode_repo.get_commit(commit_id=revision)
462 return c.rhodecode_repo.get_commit(commit_id=revision)
461 except CommitDoesNotExistError as e:
463 except CommitDoesNotExistError as e:
462 return EmptyCommit(message=str(e))
464 return EmptyCommit(message=str(e))
463 else:
465 else:
464 raise HTTPBadRequest()
466 raise HTTPBadRequest()
465
467
466 @LoginRequired()
468 @LoginRequired()
467 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
469 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
468 'repository.admin')
470 'repository.admin')
469 @jsonify
471 @jsonify
470 def changeset_children(self, repo_name, revision):
472 def changeset_children(self, repo_name, revision):
471 if request.is_xhr:
473 if request.is_xhr:
472 commit = c.rhodecode_repo.get_commit(commit_id=revision)
474 commit = c.rhodecode_repo.get_commit(commit_id=revision)
473 result = {"results": commit.children}
475 result = {"results": commit.children}
474 return result
476 return result
475 else:
477 else:
476 raise HTTPBadRequest()
478 raise HTTPBadRequest()
477
479
478 @LoginRequired()
480 @LoginRequired()
479 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
481 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
480 'repository.admin')
482 'repository.admin')
481 @jsonify
483 @jsonify
482 def changeset_parents(self, repo_name, revision):
484 def changeset_parents(self, repo_name, revision):
483 if request.is_xhr:
485 if request.is_xhr:
484 commit = c.rhodecode_repo.get_commit(commit_id=revision)
486 commit = c.rhodecode_repo.get_commit(commit_id=revision)
485 result = {"results": commit.parents}
487 result = {"results": commit.parents}
486 return result
488 return result
487 else:
489 else:
488 raise HTTPBadRequest()
490 raise HTTPBadRequest()
@@ -1,1016 +1,1018 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 comment = ChangesetComment.get_or_404(safe_int(comment_id))
990 comment = ChangesetComment.get_or_404(safe_int(comment_id))
991 if not comment:
991 if not comment:
992 log.debug('Comment with id:%s not found, skipping', comment_id)
992 log.debug('Comment with id:%s not found, skipping', comment_id)
993 # comment already deleted in another call probably
993 # comment already deleted in another call probably
994 return True
994 return True
995
995
996 if comment.pull_request.is_closed():
996 if comment.pull_request.is_closed():
997 # don't allow deleting comments on closed pull request
997 # don't allow deleting comments on closed pull request
998 raise HTTPForbidden()
998 raise HTTPForbidden()
999
999
1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1001 super_admin = h.HasPermissionAny('hg.admin')()
1001 super_admin = h.HasPermissionAny('hg.admin')()
1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1004 comment_repo_admin = is_repo_admin and is_repo_comment
1004 comment_repo_admin = is_repo_admin and is_repo_comment
1005
1005
1006 if super_admin or comment_owner or comment_repo_admin:
1006 if super_admin or comment_owner or comment_repo_admin:
1007 old_calculated_status = comment.pull_request.calculated_review_status()
1007 old_calculated_status = comment.pull_request.calculated_review_status()
1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1009 Session().commit()
1009 Session().commit()
1010 calculated_status = comment.pull_request.calculated_review_status()
1010 calculated_status = comment.pull_request.calculated_review_status()
1011 if old_calculated_status != calculated_status:
1011 if old_calculated_status != calculated_status:
1012 PullRequestModel()._trigger_pull_request_hook(
1012 PullRequestModel()._trigger_pull_request_hook(
1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1014 return True
1014 return True
1015 else:
1015 else:
1016 raise HTTPForbidden()
1016 log.warning('No permissions for user %s to delete comment_id: %s',
1017 c.rhodecode_user, comment_id)
1018 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now