##// END OF EJS Templates
pull-requests: only allow actual reviewers to leave status/votes....
marcink -
r4513:8c4f7e94 stable
parent child
Show More
@@ -1,792 +1,791
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks, channelstream
34 from rhodecode.lib import diffs, codeblocks, channelstream
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118 single_commit = len(c.commit_ranges) == 1
119
119
120 c.changes = OrderedDict()
120 c.changes = OrderedDict()
121 c.lines_added = 0
121 c.lines_added = 0
122 c.lines_deleted = 0
122 c.lines_deleted = 0
123
123
124 # auto collapse if we have more than limit
124 # auto collapse if we have more than limit
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127
127
128 c.commit_statuses = ChangesetStatus.STATUSES
128 c.commit_statuses = ChangesetStatus.STATUSES
129 c.inline_comments = []
129 c.inline_comments = []
130 c.files = []
130 c.files = []
131
131
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135
135
136 # Single commit
136 # Single commit
137 if single_commit:
137 if single_commit:
138 commit = c.commit_ranges[0]
138 commit = c.commit_ranges[0]
139 c.comments = CommentsModel().get_comments(
139 c.comments = CommentsModel().get_comments(
140 self.db_repo.repo_id,
140 self.db_repo.repo_id,
141 revision=commit.raw_id)
141 revision=commit.raw_id)
142
142
143 # comments from PR
143 # comments from PR
144 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
145 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
146 with_revisions=True)
146 with_revisions=True)
147
147
148 prs = set()
148 prs = set()
149 reviewers = list()
149 reviewers = list()
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 for c_status in statuses:
151 for c_status in statuses:
152
152
153 # extract associated pull-requests from votes
153 # extract associated pull-requests from votes
154 if c_status.pull_request:
154 if c_status.pull_request:
155 prs.add(c_status.pull_request)
155 prs.add(c_status.pull_request)
156
156
157 # extract reviewers
157 # extract reviewers
158 _user_id = c_status.author.user_id
158 _user_id = c_status.author.user_id
159 if _user_id not in reviewers_duplicates:
159 if _user_id not in reviewers_duplicates:
160 reviewers.append(
160 reviewers.append(
161 StrictAttributeDict({
161 StrictAttributeDict({
162 'user': c_status.author,
162 'user': c_status.author,
163
163
164 # fake attributed for commit, page that we don't have
164 # fake attributed for commit, page that we don't have
165 # but we share the display with PR page
165 # but we share the display with PR page
166 'mandatory': False,
166 'mandatory': False,
167 'reasons': [],
167 'reasons': [],
168 'rule_user_group_data': lambda: None
168 'rule_user_group_data': lambda: None
169 })
169 })
170 )
170 )
171 reviewers_duplicates.add(_user_id)
171 reviewers_duplicates.add(_user_id)
172
172
173 c.allowed_reviewers = reviewers
174 c.reviewers_count = len(reviewers)
173 c.reviewers_count = len(reviewers)
175 c.observers_count = 0
174 c.observers_count = 0
176
175
177 # from associated statuses, check the pull requests, and
176 # from associated statuses, check the pull requests, and
178 # show comments from them
177 # show comments from them
179 for pr in prs:
178 for pr in prs:
180 c.comments.extend(pr.comments)
179 c.comments.extend(pr.comments)
181
180
182 c.unresolved_comments = CommentsModel()\
181 c.unresolved_comments = CommentsModel()\
183 .get_commit_unresolved_todos(commit.raw_id)
182 .get_commit_unresolved_todos(commit.raw_id)
184 c.resolved_comments = CommentsModel()\
183 c.resolved_comments = CommentsModel()\
185 .get_commit_resolved_todos(commit.raw_id)
184 .get_commit_resolved_todos(commit.raw_id)
186
185
187 c.inline_comments_flat = CommentsModel()\
186 c.inline_comments_flat = CommentsModel()\
188 .get_commit_inline_comments(commit.raw_id)
187 .get_commit_inline_comments(commit.raw_id)
189
188
190 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
191 statuses, reviewers)
190 statuses, reviewers)
192
191
193 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
194
193
195 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
196
195
197 for review_obj, member, reasons, mandatory, status in review_statuses:
196 for review_obj, member, reasons, mandatory, status in review_statuses:
198 member_reviewer = h.reviewer_as_json(
197 member_reviewer = h.reviewer_as_json(
199 member, reasons=reasons, mandatory=mandatory, role=None,
198 member, reasons=reasons, mandatory=mandatory, role=None,
200 user_group=None
199 user_group=None
201 )
200 )
202
201
203 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
204 member_reviewer['review_status'] = current_review_status
203 member_reviewer['review_status'] = current_review_status
205 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
206 member_reviewer['allowed_to_update'] = False
205 member_reviewer['allowed_to_update'] = False
207 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
208
207
209 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
210
209
211 # NOTE(marcink): this uses the same voting logic as in pull-requests
210 # NOTE(marcink): this uses the same voting logic as in pull-requests
212 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
213 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
214
213
215 diff = None
214 diff = None
216 # Iterate over ranges (default commit view is always one commit)
215 # Iterate over ranges (default commit view is always one commit)
217 for commit in c.commit_ranges:
216 for commit in c.commit_ranges:
218 c.changes[commit.raw_id] = []
217 c.changes[commit.raw_id] = []
219
218
220 commit2 = commit
219 commit2 = commit
221 commit1 = commit.first_parent
220 commit1 = commit.first_parent
222
221
223 if method == 'show':
222 if method == 'show':
224 inline_comments = CommentsModel().get_inline_comments(
223 inline_comments = CommentsModel().get_inline_comments(
225 self.db_repo.repo_id, revision=commit.raw_id)
224 self.db_repo.repo_id, revision=commit.raw_id)
226 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
227 inline_comments))
226 inline_comments))
228 c.inline_comments = inline_comments
227 c.inline_comments = inline_comments
229
228
230 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
231 self.db_repo)
230 self.db_repo)
232 cache_file_path = diff_cache_exist(
231 cache_file_path = diff_cache_exist(
233 cache_path, 'diff', commit.raw_id,
232 cache_path, 'diff', commit.raw_id,
234 hide_whitespace_changes, diff_context, c.fulldiff)
233 hide_whitespace_changes, diff_context, c.fulldiff)
235
234
236 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
237 force_recache = str2bool(self.request.GET.get('force_recache'))
236 force_recache = str2bool(self.request.GET.get('force_recache'))
238
237
239 cached_diff = None
238 cached_diff = None
240 if caching_enabled:
239 if caching_enabled:
241 cached_diff = load_cached_diff(cache_file_path)
240 cached_diff = load_cached_diff(cache_file_path)
242
241
243 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
244 if not force_recache and has_proper_diff_cache:
243 if not force_recache and has_proper_diff_cache:
245 diffset = cached_diff['diff']
244 diffset = cached_diff['diff']
246 else:
245 else:
247 vcs_diff = self.rhodecode_vcs_repo.get_diff(
246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
248 commit1, commit2,
247 commit1, commit2,
249 ignore_whitespace=hide_whitespace_changes,
248 ignore_whitespace=hide_whitespace_changes,
250 context=diff_context)
249 context=diff_context)
251
250
252 diff_processor = diffs.DiffProcessor(
251 diff_processor = diffs.DiffProcessor(
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
252 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 file_limit=file_limit, show_full_diff=c.fulldiff)
253 file_limit=file_limit, show_full_diff=c.fulldiff)
255
254
256 _parsed = diff_processor.prepare()
255 _parsed = diff_processor.prepare()
257
256
258 diffset = codeblocks.DiffSet(
257 diffset = codeblocks.DiffSet(
259 repo_name=self.db_repo_name,
258 repo_name=self.db_repo_name,
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
259 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
260 target_node_getter=codeblocks.diffset_node_getter(commit2))
262
261
263 diffset = self.path_filter.render_patchset_filtered(
262 diffset = self.path_filter.render_patchset_filtered(
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265
264
266 # save cached diff
265 # save cached diff
267 if caching_enabled:
266 if caching_enabled:
268 cache_diff(cache_file_path, diffset, None)
267 cache_diff(cache_file_path, diffset, None)
269
268
270 c.limited_diff = diffset.limited_diff
269 c.limited_diff = diffset.limited_diff
271 c.changes[commit.raw_id] = diffset
270 c.changes[commit.raw_id] = diffset
272 else:
271 else:
273 # TODO(marcink): no cache usage here...
272 # TODO(marcink): no cache usage here...
274 _diff = self.rhodecode_vcs_repo.get_diff(
273 _diff = self.rhodecode_vcs_repo.get_diff(
275 commit1, commit2,
274 commit1, commit2,
276 ignore_whitespace=hide_whitespace_changes, context=diff_context)
275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
277 diff_processor = diffs.DiffProcessor(
276 diff_processor = diffs.DiffProcessor(
278 _diff, format='newdiff', diff_limit=diff_limit,
277 _diff, format='newdiff', diff_limit=diff_limit,
279 file_limit=file_limit, show_full_diff=c.fulldiff)
278 file_limit=file_limit, show_full_diff=c.fulldiff)
280 # downloads/raw we only need RAW diff nothing else
279 # downloads/raw we only need RAW diff nothing else
281 diff = self.path_filter.get_raw_patch(diff_processor)
280 diff = self.path_filter.get_raw_patch(diff_processor)
282 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
283
282
284 # sort comments by how they were generated
283 # sort comments by how they were generated
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286 c.at_version_num = None
285 c.at_version_num = None
287
286
288 if len(c.commit_ranges) == 1:
287 if len(c.commit_ranges) == 1:
289 c.commit = c.commit_ranges[0]
288 c.commit = c.commit_ranges[0]
290 c.parent_tmpl = ''.join(
289 c.parent_tmpl = ''.join(
291 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
292
291
293 if method == 'download':
292 if method == 'download':
294 response = Response(diff)
293 response = Response(diff)
295 response.content_type = 'text/plain'
294 response.content_type = 'text/plain'
296 response.content_disposition = (
295 response.content_disposition = (
297 'attachment; filename=%s.diff' % commit_id_range[:12])
296 'attachment; filename=%s.diff' % commit_id_range[:12])
298 return response
297 return response
299 elif method == 'patch':
298 elif method == 'patch':
300 c.diff = safe_unicode(diff)
299 c.diff = safe_unicode(diff)
301 patch = render(
300 patch = render(
302 'rhodecode:templates/changeset/patch_changeset.mako',
301 'rhodecode:templates/changeset/patch_changeset.mako',
303 self._get_template_context(c), self.request)
302 self._get_template_context(c), self.request)
304 response = Response(patch)
303 response = Response(patch)
305 response.content_type = 'text/plain'
304 response.content_type = 'text/plain'
306 return response
305 return response
307 elif method == 'raw':
306 elif method == 'raw':
308 response = Response(diff)
307 response = Response(diff)
309 response.content_type = 'text/plain'
308 response.content_type = 'text/plain'
310 return response
309 return response
311 elif method == 'show':
310 elif method == 'show':
312 if len(c.commit_ranges) == 1:
311 if len(c.commit_ranges) == 1:
313 html = render(
312 html = render(
314 'rhodecode:templates/changeset/changeset.mako',
313 'rhodecode:templates/changeset/changeset.mako',
315 self._get_template_context(c), self.request)
314 self._get_template_context(c), self.request)
316 return Response(html)
315 return Response(html)
317 else:
316 else:
318 c.ancestor = None
317 c.ancestor = None
319 c.target_repo = self.db_repo
318 c.target_repo = self.db_repo
320 html = render(
319 html = render(
321 'rhodecode:templates/changeset/changeset_range.mako',
320 'rhodecode:templates/changeset/changeset_range.mako',
322 self._get_template_context(c), self.request)
321 self._get_template_context(c), self.request)
323 return Response(html)
322 return Response(html)
324
323
325 raise HTTPBadRequest()
324 raise HTTPBadRequest()
326
325
327 @LoginRequired()
326 @LoginRequired()
328 @HasRepoPermissionAnyDecorator(
327 @HasRepoPermissionAnyDecorator(
329 'repository.read', 'repository.write', 'repository.admin')
328 'repository.read', 'repository.write', 'repository.admin')
330 @view_config(
329 @view_config(
331 route_name='repo_commit', request_method='GET',
330 route_name='repo_commit', request_method='GET',
332 renderer=None)
331 renderer=None)
333 def repo_commit_show(self):
332 def repo_commit_show(self):
334 commit_id = self.request.matchdict['commit_id']
333 commit_id = self.request.matchdict['commit_id']
335 return self._commit(commit_id, method='show')
334 return self._commit(commit_id, method='show')
336
335
337 @LoginRequired()
336 @LoginRequired()
338 @HasRepoPermissionAnyDecorator(
337 @HasRepoPermissionAnyDecorator(
339 'repository.read', 'repository.write', 'repository.admin')
338 'repository.read', 'repository.write', 'repository.admin')
340 @view_config(
339 @view_config(
341 route_name='repo_commit_raw', request_method='GET',
340 route_name='repo_commit_raw', request_method='GET',
342 renderer=None)
341 renderer=None)
343 @view_config(
342 @view_config(
344 route_name='repo_commit_raw_deprecated', request_method='GET',
343 route_name='repo_commit_raw_deprecated', request_method='GET',
345 renderer=None)
344 renderer=None)
346 def repo_commit_raw(self):
345 def repo_commit_raw(self):
347 commit_id = self.request.matchdict['commit_id']
346 commit_id = self.request.matchdict['commit_id']
348 return self._commit(commit_id, method='raw')
347 return self._commit(commit_id, method='raw')
349
348
350 @LoginRequired()
349 @LoginRequired()
351 @HasRepoPermissionAnyDecorator(
350 @HasRepoPermissionAnyDecorator(
352 'repository.read', 'repository.write', 'repository.admin')
351 'repository.read', 'repository.write', 'repository.admin')
353 @view_config(
352 @view_config(
354 route_name='repo_commit_patch', request_method='GET',
353 route_name='repo_commit_patch', request_method='GET',
355 renderer=None)
354 renderer=None)
356 def repo_commit_patch(self):
355 def repo_commit_patch(self):
357 commit_id = self.request.matchdict['commit_id']
356 commit_id = self.request.matchdict['commit_id']
358 return self._commit(commit_id, method='patch')
357 return self._commit(commit_id, method='patch')
359
358
360 @LoginRequired()
359 @LoginRequired()
361 @HasRepoPermissionAnyDecorator(
360 @HasRepoPermissionAnyDecorator(
362 'repository.read', 'repository.write', 'repository.admin')
361 'repository.read', 'repository.write', 'repository.admin')
363 @view_config(
362 @view_config(
364 route_name='repo_commit_download', request_method='GET',
363 route_name='repo_commit_download', request_method='GET',
365 renderer=None)
364 renderer=None)
366 def repo_commit_download(self):
365 def repo_commit_download(self):
367 commit_id = self.request.matchdict['commit_id']
366 commit_id = self.request.matchdict['commit_id']
368 return self._commit(commit_id, method='download')
367 return self._commit(commit_id, method='download')
369
368
370 @LoginRequired()
369 @LoginRequired()
371 @NotAnonymous()
370 @NotAnonymous()
372 @HasRepoPermissionAnyDecorator(
371 @HasRepoPermissionAnyDecorator(
373 'repository.read', 'repository.write', 'repository.admin')
372 'repository.read', 'repository.write', 'repository.admin')
374 @CSRFRequired()
373 @CSRFRequired()
375 @view_config(
374 @view_config(
376 route_name='repo_commit_comment_create', request_method='POST',
375 route_name='repo_commit_comment_create', request_method='POST',
377 renderer='json_ext')
376 renderer='json_ext')
378 def repo_commit_comment_create(self):
377 def repo_commit_comment_create(self):
379 _ = self.request.translate
378 _ = self.request.translate
380 commit_id = self.request.matchdict['commit_id']
379 commit_id = self.request.matchdict['commit_id']
381
380
382 c = self.load_default_context()
381 c = self.load_default_context()
383 status = self.request.POST.get('changeset_status', None)
382 status = self.request.POST.get('changeset_status', None)
384 text = self.request.POST.get('text')
383 text = self.request.POST.get('text')
385 comment_type = self.request.POST.get('comment_type')
384 comment_type = self.request.POST.get('comment_type')
386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
387
386
388 if status:
387 if status:
389 text = text or (_('Status change %(transition_icon)s %(status)s')
388 text = text or (_('Status change %(transition_icon)s %(status)s')
390 % {'transition_icon': '>',
389 % {'transition_icon': '>',
391 'status': ChangesetStatus.get_status_lbl(status)})
390 'status': ChangesetStatus.get_status_lbl(status)})
392
391
393 multi_commit_ids = []
392 multi_commit_ids = []
394 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
393 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
395 if _commit_id not in ['', None, EmptyCommit.raw_id]:
394 if _commit_id not in ['', None, EmptyCommit.raw_id]:
396 if _commit_id not in multi_commit_ids:
395 if _commit_id not in multi_commit_ids:
397 multi_commit_ids.append(_commit_id)
396 multi_commit_ids.append(_commit_id)
398
397
399 commit_ids = multi_commit_ids or [commit_id]
398 commit_ids = multi_commit_ids or [commit_id]
400
399
401 comment = None
400 comment = None
402 for current_id in filter(None, commit_ids):
401 for current_id in filter(None, commit_ids):
403 comment = CommentsModel().create(
402 comment = CommentsModel().create(
404 text=text,
403 text=text,
405 repo=self.db_repo.repo_id,
404 repo=self.db_repo.repo_id,
406 user=self._rhodecode_db_user.user_id,
405 user=self._rhodecode_db_user.user_id,
407 commit_id=current_id,
406 commit_id=current_id,
408 f_path=self.request.POST.get('f_path'),
407 f_path=self.request.POST.get('f_path'),
409 line_no=self.request.POST.get('line'),
408 line_no=self.request.POST.get('line'),
410 status_change=(ChangesetStatus.get_status_lbl(status)
409 status_change=(ChangesetStatus.get_status_lbl(status)
411 if status else None),
410 if status else None),
412 status_change_type=status,
411 status_change_type=status,
413 comment_type=comment_type,
412 comment_type=comment_type,
414 resolves_comment_id=resolves_comment_id,
413 resolves_comment_id=resolves_comment_id,
415 auth_user=self._rhodecode_user
414 auth_user=self._rhodecode_user
416 )
415 )
417 is_inline = bool(comment.f_path and comment.line_no)
416 is_inline = bool(comment.f_path and comment.line_no)
418
417
419 # get status if set !
418 # get status if set !
420 if status:
419 if status:
421 # if latest status was from pull request and it's closed
420 # if latest status was from pull request and it's closed
422 # disallow changing status !
421 # disallow changing status !
423 # dont_allow_on_closed_pull_request = True !
422 # dont_allow_on_closed_pull_request = True !
424
423
425 try:
424 try:
426 ChangesetStatusModel().set_status(
425 ChangesetStatusModel().set_status(
427 self.db_repo.repo_id,
426 self.db_repo.repo_id,
428 status,
427 status,
429 self._rhodecode_db_user.user_id,
428 self._rhodecode_db_user.user_id,
430 comment,
429 comment,
431 revision=current_id,
430 revision=current_id,
432 dont_allow_on_closed_pull_request=True
431 dont_allow_on_closed_pull_request=True
433 )
432 )
434 except StatusChangeOnClosedPullRequestError:
433 except StatusChangeOnClosedPullRequestError:
435 msg = _('Changing the status of a commit associated with '
434 msg = _('Changing the status of a commit associated with '
436 'a closed pull request is not allowed')
435 'a closed pull request is not allowed')
437 log.exception(msg)
436 log.exception(msg)
438 h.flash(msg, category='warning')
437 h.flash(msg, category='warning')
439 raise HTTPFound(h.route_path(
438 raise HTTPFound(h.route_path(
440 'repo_commit', repo_name=self.db_repo_name,
439 'repo_commit', repo_name=self.db_repo_name,
441 commit_id=current_id))
440 commit_id=current_id))
442
441
443 commit = self.db_repo.get_commit(current_id)
442 commit = self.db_repo.get_commit(current_id)
444 CommentsModel().trigger_commit_comment_hook(
443 CommentsModel().trigger_commit_comment_hook(
445 self.db_repo, self._rhodecode_user, 'create',
444 self.db_repo, self._rhodecode_user, 'create',
446 data={'comment': comment, 'commit': commit})
445 data={'comment': comment, 'commit': commit})
447
446
448 # finalize, commit and redirect
447 # finalize, commit and redirect
449 Session().commit()
448 Session().commit()
450
449
451 data = {
450 data = {
452 'target_id': h.safeid(h.safe_unicode(
451 'target_id': h.safeid(h.safe_unicode(
453 self.request.POST.get('f_path'))),
452 self.request.POST.get('f_path'))),
454 }
453 }
455 if comment:
454 if comment:
456 c.co = comment
455 c.co = comment
457 c.at_version_num = 0
456 c.at_version_num = 0
458 rendered_comment = render(
457 rendered_comment = render(
459 'rhodecode:templates/changeset/changeset_comment_block.mako',
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
460 self._get_template_context(c), self.request)
459 self._get_template_context(c), self.request)
461
460
462 data.update(comment.get_dict())
461 data.update(comment.get_dict())
463 data.update({'rendered_text': rendered_comment})
462 data.update({'rendered_text': rendered_comment})
464
463
465 comment_broadcast_channel = channelstream.comment_channel(
464 comment_broadcast_channel = channelstream.comment_channel(
466 self.db_repo_name, commit_obj=commit)
465 self.db_repo_name, commit_obj=commit)
467
466
468 comment_data = data
467 comment_data = data
469 comment_type = 'inline' if is_inline else 'general'
468 comment_type = 'inline' if is_inline else 'general'
470 channelstream.comment_channelstream_push(
469 channelstream.comment_channelstream_push(
471 self.request, comment_broadcast_channel, self._rhodecode_user,
470 self.request, comment_broadcast_channel, self._rhodecode_user,
472 _('posted a new {} comment').format(comment_type),
471 _('posted a new {} comment').format(comment_type),
473 comment_data=comment_data)
472 comment_data=comment_data)
474
473
475 return data
474 return data
476
475
477 @LoginRequired()
476 @LoginRequired()
478 @NotAnonymous()
477 @NotAnonymous()
479 @HasRepoPermissionAnyDecorator(
478 @HasRepoPermissionAnyDecorator(
480 'repository.read', 'repository.write', 'repository.admin')
479 'repository.read', 'repository.write', 'repository.admin')
481 @CSRFRequired()
480 @CSRFRequired()
482 @view_config(
481 @view_config(
483 route_name='repo_commit_comment_preview', request_method='POST',
482 route_name='repo_commit_comment_preview', request_method='POST',
484 renderer='string', xhr=True)
483 renderer='string', xhr=True)
485 def repo_commit_comment_preview(self):
484 def repo_commit_comment_preview(self):
486 # Technically a CSRF token is not needed as no state changes with this
485 # Technically a CSRF token is not needed as no state changes with this
487 # call. However, as this is a POST is better to have it, so automated
486 # call. However, as this is a POST is better to have it, so automated
488 # tools don't flag it as potential CSRF.
487 # tools don't flag it as potential CSRF.
489 # Post is required because the payload could be bigger than the maximum
488 # Post is required because the payload could be bigger than the maximum
490 # allowed by GET.
489 # allowed by GET.
491
490
492 text = self.request.POST.get('text')
491 text = self.request.POST.get('text')
493 renderer = self.request.POST.get('renderer') or 'rst'
492 renderer = self.request.POST.get('renderer') or 'rst'
494 if text:
493 if text:
495 return h.render(text, renderer=renderer, mentions=True,
494 return h.render(text, renderer=renderer, mentions=True,
496 repo_name=self.db_repo_name)
495 repo_name=self.db_repo_name)
497 return ''
496 return ''
498
497
499 @LoginRequired()
498 @LoginRequired()
500 @HasRepoPermissionAnyDecorator(
499 @HasRepoPermissionAnyDecorator(
501 'repository.read', 'repository.write', 'repository.admin')
500 'repository.read', 'repository.write', 'repository.admin')
502 @CSRFRequired()
501 @CSRFRequired()
503 @view_config(
502 @view_config(
504 route_name='repo_commit_comment_history_view', request_method='POST',
503 route_name='repo_commit_comment_history_view', request_method='POST',
505 renderer='string', xhr=True)
504 renderer='string', xhr=True)
506 def repo_commit_comment_history_view(self):
505 def repo_commit_comment_history_view(self):
507 c = self.load_default_context()
506 c = self.load_default_context()
508
507
509 comment_history_id = self.request.matchdict['comment_history_id']
508 comment_history_id = self.request.matchdict['comment_history_id']
510 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
509 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
511 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
510 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
512
511
513 if is_repo_comment:
512 if is_repo_comment:
514 c.comment_history = comment_history
513 c.comment_history = comment_history
515
514
516 rendered_comment = render(
515 rendered_comment = render(
517 'rhodecode:templates/changeset/comment_history.mako',
516 'rhodecode:templates/changeset/comment_history.mako',
518 self._get_template_context(c)
517 self._get_template_context(c)
519 , self.request)
518 , self.request)
520 return rendered_comment
519 return rendered_comment
521 else:
520 else:
522 log.warning('No permissions for user %s to show comment_history_id: %s',
521 log.warning('No permissions for user %s to show comment_history_id: %s',
523 self._rhodecode_db_user, comment_history_id)
522 self._rhodecode_db_user, comment_history_id)
524 raise HTTPNotFound()
523 raise HTTPNotFound()
525
524
526 @LoginRequired()
525 @LoginRequired()
527 @NotAnonymous()
526 @NotAnonymous()
528 @HasRepoPermissionAnyDecorator(
527 @HasRepoPermissionAnyDecorator(
529 'repository.read', 'repository.write', 'repository.admin')
528 'repository.read', 'repository.write', 'repository.admin')
530 @CSRFRequired()
529 @CSRFRequired()
531 @view_config(
530 @view_config(
532 route_name='repo_commit_comment_attachment_upload', request_method='POST',
531 route_name='repo_commit_comment_attachment_upload', request_method='POST',
533 renderer='json_ext', xhr=True)
532 renderer='json_ext', xhr=True)
534 def repo_commit_comment_attachment_upload(self):
533 def repo_commit_comment_attachment_upload(self):
535 c = self.load_default_context()
534 c = self.load_default_context()
536 upload_key = 'attachment'
535 upload_key = 'attachment'
537
536
538 file_obj = self.request.POST.get(upload_key)
537 file_obj = self.request.POST.get(upload_key)
539
538
540 if file_obj is None:
539 if file_obj is None:
541 self.request.response.status = 400
540 self.request.response.status = 400
542 return {'store_fid': None,
541 return {'store_fid': None,
543 'access_path': None,
542 'access_path': None,
544 'error': '{} data field is missing'.format(upload_key)}
543 'error': '{} data field is missing'.format(upload_key)}
545
544
546 if not hasattr(file_obj, 'filename'):
545 if not hasattr(file_obj, 'filename'):
547 self.request.response.status = 400
546 self.request.response.status = 400
548 return {'store_fid': None,
547 return {'store_fid': None,
549 'access_path': None,
548 'access_path': None,
550 'error': 'filename cannot be read from the data field'}
549 'error': 'filename cannot be read from the data field'}
551
550
552 filename = file_obj.filename
551 filename = file_obj.filename
553 file_display_name = filename
552 file_display_name = filename
554
553
555 metadata = {
554 metadata = {
556 'user_uploaded': {'username': self._rhodecode_user.username,
555 'user_uploaded': {'username': self._rhodecode_user.username,
557 'user_id': self._rhodecode_user.user_id,
556 'user_id': self._rhodecode_user.user_id,
558 'ip': self._rhodecode_user.ip_addr}}
557 'ip': self._rhodecode_user.ip_addr}}
559
558
560 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
559 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
561 allowed_extensions = [
560 allowed_extensions = [
562 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
561 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
563 '.pptx', '.txt', '.xlsx', '.zip']
562 '.pptx', '.txt', '.xlsx', '.zip']
564 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
563 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
565
564
566 try:
565 try:
567 storage = store_utils.get_file_storage(self.request.registry.settings)
566 storage = store_utils.get_file_storage(self.request.registry.settings)
568 store_uid, metadata = storage.save_file(
567 store_uid, metadata = storage.save_file(
569 file_obj.file, filename, extra_metadata=metadata,
568 file_obj.file, filename, extra_metadata=metadata,
570 extensions=allowed_extensions, max_filesize=max_file_size)
569 extensions=allowed_extensions, max_filesize=max_file_size)
571 except FileNotAllowedException:
570 except FileNotAllowedException:
572 self.request.response.status = 400
571 self.request.response.status = 400
573 permitted_extensions = ', '.join(allowed_extensions)
572 permitted_extensions = ', '.join(allowed_extensions)
574 error_msg = 'File `{}` is not allowed. ' \
573 error_msg = 'File `{}` is not allowed. ' \
575 'Only following extensions are permitted: {}'.format(
574 'Only following extensions are permitted: {}'.format(
576 filename, permitted_extensions)
575 filename, permitted_extensions)
577 return {'store_fid': None,
576 return {'store_fid': None,
578 'access_path': None,
577 'access_path': None,
579 'error': error_msg}
578 'error': error_msg}
580 except FileOverSizeException:
579 except FileOverSizeException:
581 self.request.response.status = 400
580 self.request.response.status = 400
582 limit_mb = h.format_byte_size_binary(max_file_size)
581