##// END OF EJS Templates
comments: forbig removal of comments by anyone except the owners.
milka -
r4643:6f8e3276 default
parent child Browse files
Show More
@@ -1,809 +1,813 b''
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.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps.file_store import utils as store_utils
30 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
31 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32
32
33 from rhodecode.lib import diffs, codeblocks, channelstream
33 from rhodecode.lib import diffs, codeblocks, channelstream
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.diffs import (
38 from rhodecode.lib.diffs import (
39 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 get_diff_whitespace_flag)
40 get_diff_whitespace_flag)
41 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
41 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 import rhodecode.lib.helpers as h
42 import rhodecode.lib.helpers as h
43 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict, safe_str
43 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict, safe_str
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)
46 RepositoryError, CommitDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 ChangesetCommentHistory
48 ChangesetCommentHistory
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.settings import VcsSettingsModel
52 from rhodecode.model.settings import VcsSettingsModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, request):
57 def _update_with_GET(params, request):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += request.GET.getall(k)
59 params[k] += request.GET.getall(k)
60
60
61
61
62 class RepoCommitsView(RepoAppView):
62 class RepoCommitsView(RepoAppView):
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c.rhodecode_repo = self.rhodecode_vcs_repo
65 c.rhodecode_repo = self.rhodecode_vcs_repo
66
66
67 return c
67 return c
68
68
69 def _is_diff_cache_enabled(self, target_repo):
69 def _is_diff_cache_enabled(self, target_repo):
70 caching_enabled = self._get_general_setting(
70 caching_enabled = self._get_general_setting(
71 target_repo, 'rhodecode_diff_cache')
71 target_repo, 'rhodecode_diff_cache')
72 log.debug('Diff caching enabled: %s', caching_enabled)
72 log.debug('Diff caching enabled: %s', caching_enabled)
73 return caching_enabled
73 return caching_enabled
74
74
75 def _commit(self, commit_id_range, method):
75 def _commit(self, commit_id_range, method):
76 _ = self.request.translate
76 _ = self.request.translate
77 c = self.load_default_context()
77 c = self.load_default_context()
78 c.fulldiff = self.request.GET.get('fulldiff')
78 c.fulldiff = self.request.GET.get('fulldiff')
79 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
79 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
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(safe_str(e))
112 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(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 if redirect_to_combined and not single_commit:
120 if redirect_to_combined and not single_commit:
121 source_ref = getattr(c.commit_ranges[0].parents[0]
121 source_ref = getattr(c.commit_ranges[0].parents[0]
122 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
122 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
123 target_ref = c.commit_ranges[-1].raw_id
123 target_ref = c.commit_ranges[-1].raw_id
124 next_url = h.route_path(
124 next_url = h.route_path(
125 'repo_compare',
125 'repo_compare',
126 repo_name=c.repo_name,
126 repo_name=c.repo_name,
127 source_ref_type='rev',
127 source_ref_type='rev',
128 source_ref=source_ref,
128 source_ref=source_ref,
129 target_ref_type='rev',
129 target_ref_type='rev',
130 target_ref=target_ref)
130 target_ref=target_ref)
131 raise HTTPFound(next_url)
131 raise HTTPFound(next_url)
132
132
133 c.changes = OrderedDict()
133 c.changes = OrderedDict()
134 c.lines_added = 0
134 c.lines_added = 0
135 c.lines_deleted = 0
135 c.lines_deleted = 0
136
136
137 # auto collapse if we have more than limit
137 # auto collapse if we have more than limit
138 collapse_limit = diffs.DiffProcessor._collapse_commits_over
138 collapse_limit = diffs.DiffProcessor._collapse_commits_over
139 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
139 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
140
140
141 c.commit_statuses = ChangesetStatus.STATUSES
141 c.commit_statuses = ChangesetStatus.STATUSES
142 c.inline_comments = []
142 c.inline_comments = []
143 c.files = []
143 c.files = []
144
144
145 c.comments = []
145 c.comments = []
146 c.unresolved_comments = []
146 c.unresolved_comments = []
147 c.resolved_comments = []
147 c.resolved_comments = []
148
148
149 # Single commit
149 # Single commit
150 if single_commit:
150 if single_commit:
151 commit = c.commit_ranges[0]
151 commit = c.commit_ranges[0]
152 c.comments = CommentsModel().get_comments(
152 c.comments = CommentsModel().get_comments(
153 self.db_repo.repo_id,
153 self.db_repo.repo_id,
154 revision=commit.raw_id)
154 revision=commit.raw_id)
155
155
156 # comments from PR
156 # comments from PR
157 statuses = ChangesetStatusModel().get_statuses(
157 statuses = ChangesetStatusModel().get_statuses(
158 self.db_repo.repo_id, commit.raw_id,
158 self.db_repo.repo_id, commit.raw_id,
159 with_revisions=True)
159 with_revisions=True)
160
160
161 prs = set()
161 prs = set()
162 reviewers = list()
162 reviewers = list()
163 reviewers_duplicates = set() # to not have duplicates from multiple votes
163 reviewers_duplicates = set() # to not have duplicates from multiple votes
164 for c_status in statuses:
164 for c_status in statuses:
165
165
166 # extract associated pull-requests from votes
166 # extract associated pull-requests from votes
167 if c_status.pull_request:
167 if c_status.pull_request:
168 prs.add(c_status.pull_request)
168 prs.add(c_status.pull_request)
169
169
170 # extract reviewers
170 # extract reviewers
171 _user_id = c_status.author.user_id
171 _user_id = c_status.author.user_id
172 if _user_id not in reviewers_duplicates:
172 if _user_id not in reviewers_duplicates:
173 reviewers.append(
173 reviewers.append(
174 StrictAttributeDict({
174 StrictAttributeDict({
175 'user': c_status.author,
175 'user': c_status.author,
176
176
177 # fake attributed for commit, page that we don't have
177 # fake attributed for commit, page that we don't have
178 # but we share the display with PR page
178 # but we share the display with PR page
179 'mandatory': False,
179 'mandatory': False,
180 'reasons': [],
180 'reasons': [],
181 'rule_user_group_data': lambda: None
181 'rule_user_group_data': lambda: None
182 })
182 })
183 )
183 )
184 reviewers_duplicates.add(_user_id)
184 reviewers_duplicates.add(_user_id)
185
185
186 c.reviewers_count = len(reviewers)
186 c.reviewers_count = len(reviewers)
187 c.observers_count = 0
187 c.observers_count = 0
188
188
189 # from associated statuses, check the pull requests, and
189 # from associated statuses, check the pull requests, and
190 # show comments from them
190 # show comments from them
191 for pr in prs:
191 for pr in prs:
192 c.comments.extend(pr.comments)
192 c.comments.extend(pr.comments)
193
193
194 c.unresolved_comments = CommentsModel()\
194 c.unresolved_comments = CommentsModel()\
195 .get_commit_unresolved_todos(commit.raw_id)
195 .get_commit_unresolved_todos(commit.raw_id)
196 c.resolved_comments = CommentsModel()\
196 c.resolved_comments = CommentsModel()\
197 .get_commit_resolved_todos(commit.raw_id)
197 .get_commit_resolved_todos(commit.raw_id)
198
198
199 c.inline_comments_flat = CommentsModel()\
199 c.inline_comments_flat = CommentsModel()\
200 .get_commit_inline_comments(commit.raw_id)
200 .get_commit_inline_comments(commit.raw_id)
201
201
202 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
202 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
203 statuses, reviewers)
203 statuses, reviewers)
204
204
205 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
205 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
206
206
207 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
207 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
208
208
209 for review_obj, member, reasons, mandatory, status in review_statuses:
209 for review_obj, member, reasons, mandatory, status in review_statuses:
210 member_reviewer = h.reviewer_as_json(
210 member_reviewer = h.reviewer_as_json(
211 member, reasons=reasons, mandatory=mandatory, role=None,
211 member, reasons=reasons, mandatory=mandatory, role=None,
212 user_group=None
212 user_group=None
213 )
213 )
214
214
215 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
215 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
216 member_reviewer['review_status'] = current_review_status
216 member_reviewer['review_status'] = current_review_status
217 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
217 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
218 member_reviewer['allowed_to_update'] = False
218 member_reviewer['allowed_to_update'] = False
219 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
219 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
220
220
221 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
221 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
222
222
223 # NOTE(marcink): this uses the same voting logic as in pull-requests
223 # NOTE(marcink): this uses the same voting logic as in pull-requests
224 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
224 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
225 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
225 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
226
226
227 diff = None
227 diff = None
228 # Iterate over ranges (default commit view is always one commit)
228 # Iterate over ranges (default commit view is always one commit)
229 for commit in c.commit_ranges:
229 for commit in c.commit_ranges:
230 c.changes[commit.raw_id] = []
230 c.changes[commit.raw_id] = []
231
231
232 commit2 = commit
232 commit2 = commit
233 commit1 = commit.first_parent
233 commit1 = commit.first_parent
234
234
235 if method == 'show':
235 if method == 'show':
236 inline_comments = CommentsModel().get_inline_comments(
236 inline_comments = CommentsModel().get_inline_comments(
237 self.db_repo.repo_id, revision=commit.raw_id)
237 self.db_repo.repo_id, revision=commit.raw_id)
238 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
238 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
239 inline_comments))
239 inline_comments))
240 c.inline_comments = inline_comments
240 c.inline_comments = inline_comments
241
241
242 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
242 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
243 self.db_repo)
243 self.db_repo)
244 cache_file_path = diff_cache_exist(
244 cache_file_path = diff_cache_exist(
245 cache_path, 'diff', commit.raw_id,
245 cache_path, 'diff', commit.raw_id,
246 hide_whitespace_changes, diff_context, c.fulldiff)
246 hide_whitespace_changes, diff_context, c.fulldiff)
247
247
248 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
248 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
249 force_recache = str2bool(self.request.GET.get('force_recache'))
249 force_recache = str2bool(self.request.GET.get('force_recache'))
250
250
251 cached_diff = None
251 cached_diff = None
252 if caching_enabled:
252 if caching_enabled:
253 cached_diff = load_cached_diff(cache_file_path)
253 cached_diff = load_cached_diff(cache_file_path)
254
254
255 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
255 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
256 if not force_recache and has_proper_diff_cache:
256 if not force_recache and has_proper_diff_cache:
257 diffset = cached_diff['diff']
257 diffset = cached_diff['diff']
258 else:
258 else:
259 vcs_diff = self.rhodecode_vcs_repo.get_diff(
259 vcs_diff = self.rhodecode_vcs_repo.get_diff(
260 commit1, commit2,
260 commit1, commit2,
261 ignore_whitespace=hide_whitespace_changes,
261 ignore_whitespace=hide_whitespace_changes,
262 context=diff_context)
262 context=diff_context)
263
263
264 diff_processor = diffs.DiffProcessor(
264 diff_processor = diffs.DiffProcessor(
265 vcs_diff, format='newdiff', diff_limit=diff_limit,
265 vcs_diff, format='newdiff', diff_limit=diff_limit,
266 file_limit=file_limit, show_full_diff=c.fulldiff)
266 file_limit=file_limit, show_full_diff=c.fulldiff)
267
267
268 _parsed = diff_processor.prepare()
268 _parsed = diff_processor.prepare()
269
269
270 diffset = codeblocks.DiffSet(
270 diffset = codeblocks.DiffSet(
271 repo_name=self.db_repo_name,
271 repo_name=self.db_repo_name,
272 source_node_getter=codeblocks.diffset_node_getter(commit1),
272 source_node_getter=codeblocks.diffset_node_getter(commit1),
273 target_node_getter=codeblocks.diffset_node_getter(commit2))
273 target_node_getter=codeblocks.diffset_node_getter(commit2))
274
274
275 diffset = self.path_filter.render_patchset_filtered(
275 diffset = self.path_filter.render_patchset_filtered(
276 diffset, _parsed, commit1.raw_id, commit2.raw_id)
276 diffset, _parsed, commit1.raw_id, commit2.raw_id)
277
277
278 # save cached diff
278 # save cached diff
279 if caching_enabled:
279 if caching_enabled:
280 cache_diff(cache_file_path, diffset, None)
280 cache_diff(cache_file_path, diffset, None)
281
281
282 c.limited_diff = diffset.limited_diff
282 c.limited_diff = diffset.limited_diff
283 c.changes[commit.raw_id] = diffset
283 c.changes[commit.raw_id] = diffset
284 else:
284 else:
285 # TODO(marcink): no cache usage here...
285 # TODO(marcink): no cache usage here...
286 _diff = self.rhodecode_vcs_repo.get_diff(
286 _diff = self.rhodecode_vcs_repo.get_diff(
287 commit1, commit2,
287 commit1, commit2,
288 ignore_whitespace=hide_whitespace_changes, context=diff_context)
288 ignore_whitespace=hide_whitespace_changes, context=diff_context)
289 diff_processor = diffs.DiffProcessor(
289 diff_processor = diffs.DiffProcessor(
290 _diff, format='newdiff', diff_limit=diff_limit,
290 _diff, format='newdiff', diff_limit=diff_limit,
291 file_limit=file_limit, show_full_diff=c.fulldiff)
291 file_limit=file_limit, show_full_diff=c.fulldiff)
292 # downloads/raw we only need RAW diff nothing else
292 # downloads/raw we only need RAW diff nothing else
293 diff = self.path_filter.get_raw_patch(diff_processor)
293 diff = self.path_filter.get_raw_patch(diff_processor)
294 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
294 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
295
295
296 # sort comments by how they were generated
296 # sort comments by how they were generated
297 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
297 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
298 c.at_version_num = None
298 c.at_version_num = None
299
299
300 if len(c.commit_ranges) == 1:
300 if len(c.commit_ranges) == 1:
301 c.commit = c.commit_ranges[0]
301 c.commit = c.commit_ranges[0]
302 c.parent_tmpl = ''.join(
302 c.parent_tmpl = ''.join(
303 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
303 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
304
304
305 if method == 'download':
305 if method == 'download':
306 response = Response(diff)
306 response = Response(diff)
307 response.content_type = 'text/plain'
307 response.content_type = 'text/plain'
308 response.content_disposition = (
308 response.content_disposition = (
309 'attachment; filename=%s.diff' % commit_id_range[:12])
309 'attachment; filename=%s.diff' % commit_id_range[:12])
310 return response
310 return response
311 elif method == 'patch':
311 elif method == 'patch':
312 c.diff = safe_unicode(diff)
312 c.diff = safe_unicode(diff)
313 patch = render(
313 patch = render(
314 'rhodecode:templates/changeset/patch_changeset.mako',
314 'rhodecode:templates/changeset/patch_changeset.mako',
315 self._get_template_context(c), self.request)
315 self._get_template_context(c), self.request)
316 response = Response(patch)
316 response = Response(patch)
317 response.content_type = 'text/plain'
317 response.content_type = 'text/plain'
318 return response
318 return response
319 elif method == 'raw':
319 elif method == 'raw':
320 response = Response(diff)
320 response = Response(diff)
321 response.content_type = 'text/plain'
321 response.content_type = 'text/plain'
322 return response
322 return response
323 elif method == 'show':
323 elif method == 'show':
324 if len(c.commit_ranges) == 1:
324 if len(c.commit_ranges) == 1:
325 html = render(
325 html = render(
326 'rhodecode:templates/changeset/changeset.mako',
326 'rhodecode:templates/changeset/changeset.mako',
327 self._get_template_context(c), self.request)
327 self._get_template_context(c), self.request)
328 return Response(html)
328 return Response(html)
329 else:
329 else:
330 c.ancestor = None
330 c.ancestor = None
331 c.target_repo = self.db_repo
331 c.target_repo = self.db_repo
332 html = render(
332 html = render(
333 'rhodecode:templates/changeset/changeset_range.mako',
333 'rhodecode:templates/changeset/changeset_range.mako',
334 self._get_template_context(c), self.request)
334 self._get_template_context(c), self.request)
335 return Response(html)
335 return Response(html)
336
336
337 raise HTTPBadRequest()
337 raise HTTPBadRequest()
338
338
339 @LoginRequired()
339 @LoginRequired()
340 @HasRepoPermissionAnyDecorator(
340 @HasRepoPermissionAnyDecorator(
341 'repository.read', 'repository.write', 'repository.admin')
341 'repository.read', 'repository.write', 'repository.admin')
342 def repo_commit_show(self):
342 def repo_commit_show(self):
343 commit_id = self.request.matchdict['commit_id']
343 commit_id = self.request.matchdict['commit_id']
344 return self._commit(commit_id, method='show')
344 return self._commit(commit_id, method='show')
345
345
346 @LoginRequired()
346 @LoginRequired()
347 @HasRepoPermissionAnyDecorator(
347 @HasRepoPermissionAnyDecorator(
348 'repository.read', 'repository.write', 'repository.admin')
348 'repository.read', 'repository.write', 'repository.admin')
349 def repo_commit_raw(self):
349 def repo_commit_raw(self):
350 commit_id = self.request.matchdict['commit_id']
350 commit_id = self.request.matchdict['commit_id']
351 return self._commit(commit_id, method='raw')
351 return self._commit(commit_id, method='raw')
352
352
353 @LoginRequired()
353 @LoginRequired()
354 @HasRepoPermissionAnyDecorator(
354 @HasRepoPermissionAnyDecorator(
355 'repository.read', 'repository.write', 'repository.admin')
355 'repository.read', 'repository.write', 'repository.admin')
356 def repo_commit_patch(self):
356 def repo_commit_patch(self):
357 commit_id = self.request.matchdict['commit_id']
357 commit_id = self.request.matchdict['commit_id']
358 return self._commit(commit_id, method='patch')
358 return self._commit(commit_id, method='patch')
359
359
360 @LoginRequired()
360 @LoginRequired()
361 @HasRepoPermissionAnyDecorator(
361 @HasRepoPermissionAnyDecorator(
362 'repository.read', 'repository.write', 'repository.admin')
362 'repository.read', 'repository.write', 'repository.admin')
363 def repo_commit_download(self):
363 def repo_commit_download(self):
364 commit_id = self.request.matchdict['commit_id']
364 commit_id = self.request.matchdict['commit_id']
365 return self._commit(commit_id, method='download')
365 return self._commit(commit_id, method='download')
366
366
367 def _commit_comments_create(self, commit_id, comments):
367 def _commit_comments_create(self, commit_id, comments):
368 _ = self.request.translate
368 _ = self.request.translate
369 data = {}
369 data = {}
370 if not comments:
370 if not comments:
371 return
371 return
372
372
373 commit = self.db_repo.get_commit(commit_id)
373 commit = self.db_repo.get_commit(commit_id)
374
374
375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
376 for entry in comments:
376 for entry in comments:
377 c = self.load_default_context()
377 c = self.load_default_context()
378 comment_type = entry['comment_type']
378 comment_type = entry['comment_type']
379 text = entry['text']
379 text = entry['text']
380 status = entry['status']
380 status = entry['status']
381 is_draft = str2bool(entry['is_draft'])
381 is_draft = str2bool(entry['is_draft'])
382 resolves_comment_id = entry['resolves_comment_id']
382 resolves_comment_id = entry['resolves_comment_id']
383 f_path = entry['f_path']
383 f_path = entry['f_path']
384 line_no = entry['line']
384 line_no = entry['line']
385 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
385 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
386
386
387 if status:
387 if status:
388 text = text or (_('Status change %(transition_icon)s %(status)s')
388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 % {'transition_icon': '>',
389 % {'transition_icon': '>',
390 'status': ChangesetStatus.get_status_lbl(status)})
390 'status': ChangesetStatus.get_status_lbl(status)})
391
391
392 comment = CommentsModel().create(
392 comment = CommentsModel().create(
393 text=text,
393 text=text,
394 repo=self.db_repo.repo_id,
394 repo=self.db_repo.repo_id,
395 user=self._rhodecode_db_user.user_id,
395 user=self._rhodecode_db_user.user_id,
396 commit_id=commit_id,
396 commit_id=commit_id,
397 f_path=f_path,
397 f_path=f_path,
398 line_no=line_no,
398 line_no=line_no,
399 status_change=(ChangesetStatus.get_status_lbl(status)
399 status_change=(ChangesetStatus.get_status_lbl(status)
400 if status else None),
400 if status else None),
401 status_change_type=status,
401 status_change_type=status,
402 comment_type=comment_type,
402 comment_type=comment_type,
403 is_draft=is_draft,
403 is_draft=is_draft,
404 resolves_comment_id=resolves_comment_id,
404 resolves_comment_id=resolves_comment_id,
405 auth_user=self._rhodecode_user,
405 auth_user=self._rhodecode_user,
406 send_email=not is_draft, # skip notification for draft comments
406 send_email=not is_draft, # skip notification for draft comments
407 )
407 )
408 is_inline = comment.is_inline
408 is_inline = comment.is_inline
409
409
410 # get status if set !
410 # get status if set !
411 if status:
411 if status:
412 # `dont_allow_on_closed_pull_request = True` means
412 # `dont_allow_on_closed_pull_request = True` means
413 # if latest status was from pull request and it's closed
413 # if latest status was from pull request and it's closed
414 # disallow changing status !
414 # disallow changing status !
415
415
416 try:
416 try:
417 ChangesetStatusModel().set_status(
417 ChangesetStatusModel().set_status(
418 self.db_repo.repo_id,
418 self.db_repo.repo_id,
419 status,
419 status,
420 self._rhodecode_db_user.user_id,
420 self._rhodecode_db_user.user_id,
421 comment,
421 comment,
422 revision=commit_id,
422 revision=commit_id,
423 dont_allow_on_closed_pull_request=True
423 dont_allow_on_closed_pull_request=True
424 )
424 )
425 except StatusChangeOnClosedPullRequestError:
425 except StatusChangeOnClosedPullRequestError:
426 msg = _('Changing the status of a commit associated with '
426 msg = _('Changing the status of a commit associated with '
427 'a closed pull request is not allowed')
427 'a closed pull request is not allowed')
428 log.exception(msg)
428 log.exception(msg)
429 h.flash(msg, category='warning')
429 h.flash(msg, category='warning')
430 raise HTTPFound(h.route_path(
430 raise HTTPFound(h.route_path(
431 'repo_commit', repo_name=self.db_repo_name,
431 'repo_commit', repo_name=self.db_repo_name,
432 commit_id=commit_id))
432 commit_id=commit_id))
433
433
434 Session().flush()
434 Session().flush()
435 # this is somehow required to get access to some relationship
435 # this is somehow required to get access to some relationship
436 # loaded on comment
436 # loaded on comment
437 Session().refresh(comment)
437 Session().refresh(comment)
438
438
439 # skip notifications for drafts
439 # skip notifications for drafts
440 if not is_draft:
440 if not is_draft:
441 CommentsModel().trigger_commit_comment_hook(
441 CommentsModel().trigger_commit_comment_hook(
442 self.db_repo, self._rhodecode_user, 'create',
442 self.db_repo, self._rhodecode_user, 'create',
443 data={'comment': comment, 'commit': commit})
443 data={'comment': comment, 'commit': commit})
444
444
445 comment_id = comment.comment_id
445 comment_id = comment.comment_id
446 data[comment_id] = {
446 data[comment_id] = {
447 'target_id': target_elem_id
447 'target_id': target_elem_id
448 }
448 }
449 Session().flush()
449 Session().flush()
450
450
451 c.co = comment
451 c.co = comment
452 c.at_version_num = 0
452 c.at_version_num = 0
453 c.is_new = True
453 c.is_new = True
454 rendered_comment = render(
454 rendered_comment = render(
455 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 'rhodecode:templates/changeset/changeset_comment_block.mako',
456 self._get_template_context(c), self.request)
456 self._get_template_context(c), self.request)
457
457
458 data[comment_id].update(comment.get_dict())
458 data[comment_id].update(comment.get_dict())
459 data[comment_id].update({'rendered_text': rendered_comment})
459 data[comment_id].update({'rendered_text': rendered_comment})
460
460
461 # finalize, commit and redirect
461 # finalize, commit and redirect
462 Session().commit()
462 Session().commit()
463
463
464 # skip channelstream for draft comments
464 # skip channelstream for draft comments
465 if not all_drafts:
465 if not all_drafts:
466 comment_broadcast_channel = channelstream.comment_channel(
466 comment_broadcast_channel = channelstream.comment_channel(
467 self.db_repo_name, commit_obj=commit)
467 self.db_repo_name, commit_obj=commit)
468
468
469 comment_data = data
469 comment_data = data
470 posted_comment_type = 'inline' if is_inline else 'general'
470 posted_comment_type = 'inline' if is_inline else 'general'
471 if len(data) == 1:
471 if len(data) == 1:
472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
473 else:
473 else:
474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
475
475
476 channelstream.comment_channelstream_push(
476 channelstream.comment_channelstream_push(
477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
478 comment_data=comment_data)
478 comment_data=comment_data)
479
479
480 return data
480 return data
481
481
482 @LoginRequired()
482 @LoginRequired()
483 @NotAnonymous()
483 @NotAnonymous()
484 @HasRepoPermissionAnyDecorator(
484 @HasRepoPermissionAnyDecorator(
485 'repository.read', 'repository.write', 'repository.admin')
485 'repository.read', 'repository.write', 'repository.admin')
486 @CSRFRequired()
486 @CSRFRequired()
487 def repo_commit_comment_create(self):
487 def repo_commit_comment_create(self):
488 _ = self.request.translate
488 _ = self.request.translate
489 commit_id = self.request.matchdict['commit_id']
489 commit_id = self.request.matchdict['commit_id']
490
490
491 multi_commit_ids = []
491 multi_commit_ids = []
492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
494 if _commit_id not in multi_commit_ids:
494 if _commit_id not in multi_commit_ids:
495 multi_commit_ids.append(_commit_id)
495 multi_commit_ids.append(_commit_id)
496
496
497 commit_ids = multi_commit_ids or [commit_id]
497 commit_ids = multi_commit_ids or [commit_id]
498
498
499 data = []
499 data = []
500 # Multiple comments for each passed commit id
500 # Multiple comments for each passed commit id
501 for current_id in filter(None, commit_ids):
501 for current_id in filter(None, commit_ids):
502 comment_data = {
502 comment_data = {
503 'comment_type': self.request.POST.get('comment_type'),
503 'comment_type': self.request.POST.get('comment_type'),
504 'text': self.request.POST.get('text'),
504 'text': self.request.POST.get('text'),
505 'status': self.request.POST.get('changeset_status', None),
505 'status': self.request.POST.get('changeset_status', None),
506 'is_draft': self.request.POST.get('draft'),
506 'is_draft': self.request.POST.get('draft'),
507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
508 'close_pull_request': self.request.POST.get('close_pull_request'),
508 'close_pull_request': self.request.POST.get('close_pull_request'),
509 'f_path': self.request.POST.get('f_path'),
509 'f_path': self.request.POST.get('f_path'),
510 'line': self.request.POST.get('line'),
510 'line': self.request.POST.get('line'),
511 }
511 }
512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
513 data.append(comment)
513 data.append(comment)
514
514
515 return data if len(data) > 1 else data[0]
515 return data if len(data) > 1 else data[0]
516
516
517 @LoginRequired()
517 @LoginRequired()
518 @NotAnonymous()
518 @NotAnonymous()
519 @HasRepoPermissionAnyDecorator(
519 @HasRepoPermissionAnyDecorator(
520 'repository.read', 'repository.write', 'repository.admin')
520 'repository.read', 'repository.write', 'repository.admin')
521 @CSRFRequired()
521 @CSRFRequired()
522 def repo_commit_comment_preview(self):
522 def repo_commit_comment_preview(self):
523 # Technically a CSRF token is not needed as no state changes with this
523 # Technically a CSRF token is not needed as no state changes with this
524 # call. However, as this is a POST is better to have it, so automated
524 # call. However, as this is a POST is better to have it, so automated
525 # tools don't flag it as potential CSRF.
525 # tools don't flag it as potential CSRF.
526 # Post is required because the payload could be bigger than the maximum
526 # Post is required because the payload could be bigger than the maximum
527 # allowed by GET.
527 # allowed by GET.
528
528
529 text = self.request.POST.get('text')
529 text = self.request.POST.get('text')
530 renderer = self.request.POST.get('renderer') or 'rst'
530 renderer = self.request.POST.get('renderer') or 'rst'
531 if text:
531 if text:
532 return h.render(text, renderer=renderer, mentions=True,
532 return h.render(text, renderer=renderer, mentions=True,
533 repo_name=self.db_repo_name)
533 repo_name=self.db_repo_name)
534 return ''
534 return ''
535
535
536 @LoginRequired()
536 @LoginRequired()
537 @HasRepoPermissionAnyDecorator(
537 @HasRepoPermissionAnyDecorator(
538 'repository.read', 'repository.write', 'repository.admin')
538 'repository.read', 'repository.write', 'repository.admin')
539 @CSRFRequired()
539 @CSRFRequired()
540 def repo_commit_comment_history_view(self):
540 def repo_commit_comment_history_view(self):
541 c = self.load_default_context()
541 c = self.load_default_context()
542
542
543 comment_history_id = self.request.matchdict['comment_history_id']
543 comment_history_id = self.request.matchdict['comment_history_id']
544 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
544 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
545 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
545 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
546
546
547 if is_repo_comment:
547 if is_repo_comment:
548 c.comment_history = comment_history
548 c.comment_history = comment_history
549
549
550 rendered_comment = render(
550 rendered_comment = render(
551 'rhodecode:templates/changeset/comment_history.mako',
551 'rhodecode:templates/changeset/comment_history.mako',
552 self._get_template_context(c)
552 self._get_template_context(c)
553 , self.request)
553 , self.request)
554 return rendered_comment
554 return rendered_comment
555 else:
555 else:
556 log.warning('No permissions for user %s to show comment_history_id: %s',
556 log.warning('No permissions for user %s to show comment_history_id: %s',
557 self._rhodecode_db_user, comment_history_id)
557 self._rhodecode_db_user, comment_history_id)
558 raise HTTPNotFound()
558 raise HTTPNotFound()
559
559
560 @LoginRequired()
560 @LoginRequired()
561 @NotAnonymous()
561 @NotAnonymous()
562 @HasRepoPermissionAnyDecorator(
562 @HasRepoPermissionAnyDecorator(
563 'repository.read', 'repository.write', 'repository.admin')
563 'repository.read', 'repository.write', 'repository.admin')
564 @CSRFRequired()
564 @CSRFRequired()
565 def repo_commit_comment_attachment_upload(self):
565 def repo_commit_comment_attachment_upload(self):
566 c = self.load_default_context()
566 c = self.load_default_context()
567 upload_key = 'attachment'
567 upload_key = 'attachment'
568
568
569 file_obj = self.request.POST.get(upload_key)
569 file_obj = self.request.POST.get(upload_key)
570
570
571 if file_obj is None:
571 if file_obj is None:
572 self.request.response.status = 400
572 self.request.response.status = 400
573 return {'store_fid': None,
573 return {'store_fid': None,
574 'access_path': None,
574 'access_path': None,
575 'error': '{} data field is missing'.format(upload_key)}
575 'error': '{} data field is missing'.format(upload_key)}
576
576
577 if not hasattr(file_obj, 'filename'):
577 if not hasattr(file_obj, 'filename'):
578 self.request.response.status = 400
578 self.request.response.status = 400
579 return {'store_fid': None,
579 return {'store_fid': None,
580 'access_path': None,
580 'access_path': None,
581 'error': 'filename cannot be read from the data field'}
581 'error': 'filename cannot be read from the data field'}
582
582
583 filename = file_obj.filename
583 filename = file_obj.filename
584 file_display_name = filename
584 file_display_name = filename
585
585
586 metadata = {
586 metadata = {
587 'user_uploaded': {'username': self._rhodecode_user.username,
587 'user_uploaded': {'username': self._rhodecode_user.username,
588 'user_id': self._rhodecode_user.user_id,
588 'user_id': self._rhodecode_user.user_id,
589 'ip': self._rhodecode_user.ip_addr}}
589 'ip': self._rhodecode_user.ip_addr}}
590
590
591 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
591 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
592 allowed_extensions = [
592 allowed_extensions = [
593 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
593 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
594 '.pptx', '.txt', '.xlsx', '.zip']
594 '.pptx', '.txt', '.xlsx', '.zip']
595 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
595 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
596
596
597 try:
597 try:
598 storage = store_utils.get_file_storage(self.request.registry.settings)
598 storage = store_utils.get_file_storage(self.request.registry.settings)
599 store_uid, metadata = storage.save_file(
599 store_uid, metadata = storage.save_file(
600 file_obj.file, filename, extra_metadata=metadata,
600 file_obj.file, filename, extra_metadata=metadata,
601 extensions=allowed_extensions, max_filesize=max_file_size)
601 extensions=allowed_extensions, max_filesize=max_file_size)
602 except FileNotAllowedException:
602 except FileNotAllowedException:
603 self.request.response.status = 400
603 self.request.response.status = 400
604 permitted_extensions = ', '.join(allowed_extensions)
604 permitted_extensions = ', '.join(allowed_extensions)
605 error_msg = 'File `{}` is not allowed. ' \
605 error_msg = 'File `{}` is not allowed. ' \
606 'Only following extensions are permitted: {}'.format(
606 'Only following extensions are permitted: {}'.format(
607 filename, permitted_extensions)
607 filename, permitted_extensions)
608 return {'store_fid': None,
608 return {'store_fid': None,
609 'access_path': None,
609 'access_path': None,
610 'error': error_msg}
610 'error': error_msg}
611 except FileOverSizeException:
611 except FileOverSizeException:
612 self.request.response.status = 400
612 self.request.response.status = 400
613 limit_mb = h.format_byte_size_binary(max_file_size)
613 limit_mb = h.format_byte_size_binary(max_file_size)
614 return {'store_fid': None,
614 return {'store_fid': None,
615 'access_path': None,
615 'access_path': None,
616 'error': 'File {} is exceeding allowed limit of {}.'.format(
616 'error': 'File {} is exceeding allowed limit of {}.'.format(
617 filename, limit_mb)}
617 filename, limit_mb)}
618
618
619 try:
619 try:
620 entry = FileStore.create(
620 entry = FileStore.create(
621 file_uid=store_uid, filename=metadata["filename"],
621 file_uid=store_uid, filename=metadata["filename"],
622 file_hash=metadata["sha256"], file_size=metadata["size"],
622 file_hash=metadata["sha256"], file_size=metadata["size"],
623 file_display_name=file_display_name,
623 file_display_name=file_display_name,
624 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
624 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
625 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
625 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
626 scope_repo_id=self.db_repo.repo_id
626 scope_repo_id=self.db_repo.repo_id
627 )
627 )
628 Session().add(entry)
628 Session().add(entry)
629 Session().commit()
629 Session().commit()
630 log.debug('Stored upload in DB as %s', entry)
630 log.debug('Stored upload in DB as %s', entry)
631 except Exception:
631 except Exception:
632 log.exception('Failed to store file %s', filename)
632 log.exception('Failed to store file %s', filename)
633 self.request.response.status = 400
633 self.request.response.status = 400
634 return {'store_fid': None,
634 return {'store_fid': None,
635 'access_path': None,
635 'access_path': None,
636 'error': 'File {} failed to store in DB.'.format(filename)}
636 'error': 'File {} failed to store in DB.'.format(filename)}
637
637
638 Session().commit()
638 Session().commit()
639
639
640 return {
640 return {
641 'store_fid': store_uid,
641 'store_fid': store_uid,
642 'access_path': h.route_path(
642 'access_path': h.route_path(
643 'download_file', fid=store_uid),
643 'download_file', fid=store_uid),
644 'fqn_access_path': h.route_url(
644 'fqn_access_path': h.route_url(
645 'download_file', fid=store_uid),
645 'download_file', fid=store_uid),
646 'repo_access_path': h.route_path(
646 'repo_access_path': h.route_path(
647 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
647 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
648 'repo_fqn_access_path': h.route_url(
648 'repo_fqn_access_path': h.route_url(
649 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
649 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
650 }
650 }
651
651
652 @LoginRequired()
652 @LoginRequired()
653 @NotAnonymous()
653 @NotAnonymous()
654 @HasRepoPermissionAnyDecorator(
654 @HasRepoPermissionAnyDecorator(
655 'repository.read', 'repository.write', 'repository.admin')
655 'repository.read', 'repository.write', 'repository.admin')
656 @CSRFRequired()
656 @CSRFRequired()
657 def repo_commit_comment_delete(self):
657 def repo_commit_comment_delete(self):
658 commit_id = self.request.matchdict['commit_id']
658 commit_id = self.request.matchdict['commit_id']
659 comment_id = self.request.matchdict['comment_id']
659 comment_id = self.request.matchdict['comment_id']
660
660
661 comment = ChangesetComment.get_or_404(comment_id)
661 comment = ChangesetComment.get_or_404(comment_id)
662 if not comment:
662 if not comment:
663 log.debug('Comment with id:%s not found, skipping', comment_id)
663 log.debug('Comment with id:%s not found, skipping', comment_id)
664 # comment already deleted in another call probably
664 # comment already deleted in another call probably
665 return True
665 return True
666
666
667 if comment.immutable:
667 if comment.immutable:
668 # don't allow deleting comments that are immutable
668 # don't allow deleting comments that are immutable
669 raise HTTPForbidden()
669 raise HTTPForbidden()
670
670
671 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
671 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
672 super_admin = h.HasPermissionAny('hg.admin')()
672 super_admin = h.HasPermissionAny('hg.admin')()
673 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
673 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
674 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
674 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
675 comment_repo_admin = is_repo_admin and is_repo_comment
675 comment_repo_admin = is_repo_admin and is_repo_comment
676
676
677 if comment.draft and not comment_owner:
678 # We never allow to delete draft comments for other than owners
679 raise HTTPNotFound()
680
677 if super_admin or comment_owner or comment_repo_admin:
681 if super_admin or comment_owner or comment_repo_admin:
678 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
682 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
679 Session().commit()
683 Session().commit()
680 return True
684 return True
681 else:
685 else:
682 log.warning('No permissions for user %s to delete comment_id: %s',
686 log.warning('No permissions for user %s to delete comment_id: %s',
683 self._rhodecode_db_user, comment_id)
687 self._rhodecode_db_user, comment_id)
684 raise HTTPNotFound()
688 raise HTTPNotFound()
685
689
686 @LoginRequired()
690 @LoginRequired()
687 @NotAnonymous()
691 @NotAnonymous()
688 @HasRepoPermissionAnyDecorator(
692 @HasRepoPermissionAnyDecorator(
689 'repository.read', 'repository.write', 'repository.admin')
693 'repository.read', 'repository.write', 'repository.admin')
690 @CSRFRequired()
694 @CSRFRequired()
691 def repo_commit_comment_edit(self):
695 def repo_commit_comment_edit(self):
692 self.load_default_context()
696 self.load_default_context()
693
697
694 commit_id = self.request.matchdict['commit_id']
698 commit_id = self.request.matchdict['commit_id']
695 comment_id = self.request.matchdict['comment_id']
699 comment_id = self.request.matchdict['comment_id']
696 comment = ChangesetComment.get_or_404(comment_id)
700 comment = ChangesetComment.get_or_404(comment_id)
697
701
698 if comment.immutable:
702 if comment.immutable:
699 # don't allow deleting comments that are immutable
703 # don't allow deleting comments that are immutable
700 raise HTTPForbidden()
704 raise HTTPForbidden()
701
705
702 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
706 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
703 super_admin = h.HasPermissionAny('hg.admin')()
707 super_admin = h.HasPermissionAny('hg.admin')()
704 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
708 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
705 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
709 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
706 comment_repo_admin = is_repo_admin and is_repo_comment
710 comment_repo_admin = is_repo_admin and is_repo_comment
707
711
708 if super_admin or comment_owner or comment_repo_admin:
712 if super_admin or comment_owner or comment_repo_admin:
709 text = self.request.POST.get('text')
713 text = self.request.POST.get('text')
710 version = self.request.POST.get('version')
714 version = self.request.POST.get('version')
711 if text == comment.text:
715 if text == comment.text:
712 log.warning(
716 log.warning(
713 'Comment(repo): '
717 'Comment(repo): '
714 'Trying to create new version '
718 'Trying to create new version '
715 'with the same comment body {}'.format(
719 'with the same comment body {}'.format(
716 comment_id,
720 comment_id,
717 )
721 )
718 )
722 )
719 raise HTTPNotFound()
723 raise HTTPNotFound()
720
724
721 if version.isdigit():
725 if version.isdigit():
722 version = int(version)
726 version = int(version)
723 else:
727 else:
724 log.warning(
728 log.warning(
725 'Comment(repo): Wrong version type {} {} '
729 'Comment(repo): Wrong version type {} {} '
726 'for comment {}'.format(
730 'for comment {}'.format(
727 version,
731 version,
728 type(version),
732 type(version),
729 comment_id,
733 comment_id,
730 )
734 )
731 )
735 )
732 raise HTTPNotFound()
736 raise HTTPNotFound()
733
737
734 try:
738 try:
735 comment_history = CommentsModel().edit(
739 comment_history = CommentsModel().edit(
736 comment_id=comment_id,
740 comment_id=comment_id,
737 text=text,
741 text=text,
738 auth_user=self._rhodecode_user,
742 auth_user=self._rhodecode_user,
739 version=version,
743 version=version,
740 )
744 )
741 except CommentVersionMismatch:
745 except CommentVersionMismatch:
742 raise HTTPConflict()
746 raise HTTPConflict()
743
747
744 if not comment_history:
748 if not comment_history:
745 raise HTTPNotFound()
749 raise HTTPNotFound()
746
750
747 if not comment.draft:
751 if not comment.draft:
748 commit = self.db_repo.get_commit(commit_id)
752 commit = self.db_repo.get_commit(commit_id)
749 CommentsModel().trigger_commit_comment_hook(
753 CommentsModel().trigger_commit_comment_hook(
750 self.db_repo, self._rhodecode_user, 'edit',
754 self.db_repo, self._rhodecode_user, 'edit',
751 data={'comment': comment, 'commit': commit})
755 data={'comment': comment, 'commit': commit})
752
756
753 Session().commit()
757 Session().commit()
754 return {
758 return {
755 'comment_history_id': comment_history.comment_history_id,
759 'comment_history_id': comment_history.comment_history_id,
756 'comment_id': comment.comment_id,
760 'comment_id': comment.comment_id,
757 'comment_version': comment_history.version,
761 'comment_version': comment_history.version,
758 'comment_author_username': comment_history.author.username,
762 'comment_author_username': comment_history.author.username,
759 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
763 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
760 'comment_created_on': h.age_component(comment_history.created_on,
764 'comment_created_on': h.age_component(comment_history.created_on,
761 time_is_local=True),
765 time_is_local=True),
762 }
766 }
763 else:
767 else:
764 log.warning('No permissions for user %s to edit comment_id: %s',
768 log.warning('No permissions for user %s to edit comment_id: %s',
765 self._rhodecode_db_user, comment_id)
769 self._rhodecode_db_user, comment_id)
766 raise HTTPNotFound()
770 raise HTTPNotFound()
767
771
768 @LoginRequired()
772 @LoginRequired()
769 @HasRepoPermissionAnyDecorator(
773 @HasRepoPermissionAnyDecorator(
770 'repository.read', 'repository.write', 'repository.admin')
774 'repository.read', 'repository.write', 'repository.admin')
771 def repo_commit_data(self):
775 def repo_commit_data(self):
772 commit_id = self.request.matchdict['commit_id']
776 commit_id = self.request.matchdict['commit_id']
773 self.load_default_context()
777 self.load_default_context()
774
778
775 try:
779 try:
776 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
780 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
777 except CommitDoesNotExistError as e:
781 except CommitDoesNotExistError as e:
778 return EmptyCommit(message=str(e))
782 return EmptyCommit(message=str(e))
779
783
780 @LoginRequired()
784 @LoginRequired()
781 @HasRepoPermissionAnyDecorator(
785 @HasRepoPermissionAnyDecorator(
782 'repository.read', 'repository.write', 'repository.admin')
786 'repository.read', 'repository.write', 'repository.admin')
783 def repo_commit_children(self):
787 def repo_commit_children(self):
784 commit_id = self.request.matchdict['commit_id']
788 commit_id = self.request.matchdict['commit_id']
785 self.load_default_context()
789 self.load_default_context()
786
790
787 try:
791 try:
788 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
792 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
789 children = commit.children
793 children = commit.children
790 except CommitDoesNotExistError:
794 except CommitDoesNotExistError:
791 children = []
795 children = []
792
796
793 result = {"results": children}
797 result = {"results": children}
794 return result
798 return result
795
799
796 @LoginRequired()
800 @LoginRequired()
797 @HasRepoPermissionAnyDecorator(
801 @HasRepoPermissionAnyDecorator(
798 'repository.read', 'repository.write', 'repository.admin')
802 'repository.read', 'repository.write', 'repository.admin')
799 def repo_commit_parents(self):
803 def repo_commit_parents(self):
800 commit_id = self.request.matchdict['commit_id']
804 commit_id = self.request.matchdict['commit_id']
801 self.load_default_context()
805 self.load_default_context()
802
806
803 try:
807 try:
804 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
808 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
805 parents = commit.parents
809 parents = commit.parents
806 except CommitDoesNotExistError:
810 except CommitDoesNotExistError:
807 parents = []
811 parents = []
808 result = {"results": parents}
812 result = {"results": parents}
809 return result
813 return result
@@ -1,1857 +1,1861 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-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 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29
29
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 statuses=statuses, offset=start, length=limit,
83 statuses=statuses, offset=start, length=limit,
84 order_by=order_by, order_dir=order_dir)
84 order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 opened_by=opened_by)
87 opened_by=opened_by)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by,
93 order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
96 statuses=statuses, opened_by=opened_by)
97 else:
97 else:
98 pull_requests = PullRequestModel().get_all(
98 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 statuses=statuses, offset=start, length=limit,
100 statuses=statuses, offset=start, length=limit,
101 order_by=order_by, order_dir=order_dir)
101 order_by=order_by, order_dir=order_dir)
102 pull_requests_total_count = PullRequestModel().count_all(
102 pull_requests_total_count = PullRequestModel().count_all(
103 repo_name, search_q=search_q, source=source, statuses=statuses,
103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 opened_by=opened_by)
104 opened_by=opened_by)
105
105
106 data = []
106 data = []
107 comments_model = CommentsModel()
107 comments_model = CommentsModel()
108 for pr in pull_requests:
108 for pr in pull_requests:
109 comments_count = comments_model.get_all_comments(
109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr,
110 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
111 include_drafts=False, count_only=True)
112
112
113 data.append({
113 data.append({
114 'name': _render('pullrequest_name',
114 'name': _render('pullrequest_name',
115 pr.pull_request_id, pr.pull_request_state,
115 pr.pull_request_id, pr.pull_request_state,
116 pr.work_in_progress, pr.target_repo.repo_name,
116 pr.work_in_progress, pr.target_repo.repo_name,
117 short=True),
117 short=True),
118 'name_raw': pr.pull_request_id,
118 'name_raw': pr.pull_request_id,
119 'status': _render('pullrequest_status',
119 'status': _render('pullrequest_status',
120 pr.calculated_review_status()),
120 pr.calculated_review_status()),
121 'title': _render('pullrequest_title', pr.title, pr.description),
121 'title': _render('pullrequest_title', pr.title, pr.description),
122 'description': h.escape(pr.description),
122 'description': h.escape(pr.description),
123 'updated_on': _render('pullrequest_updated_on',
123 'updated_on': _render('pullrequest_updated_on',
124 h.datetime_to_time(pr.updated_on),
124 h.datetime_to_time(pr.updated_on),
125 pr.versions_count),
125 pr.versions_count),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 'created_on': _render('pullrequest_updated_on',
127 'created_on': _render('pullrequest_updated_on',
128 h.datetime_to_time(pr.created_on)),
128 h.datetime_to_time(pr.created_on)),
129 'created_on_raw': h.datetime_to_time(pr.created_on),
129 'created_on_raw': h.datetime_to_time(pr.created_on),
130 'state': pr.pull_request_state,
130 'state': pr.pull_request_state,
131 'author': _render('pullrequest_author',
131 'author': _render('pullrequest_author',
132 pr.author.full_contact, ),
132 pr.author.full_contact, ),
133 'author_raw': pr.author.full_name,
133 'author_raw': pr.author.full_name,
134 'comments': _render('pullrequest_comments', comments_count),
134 'comments': _render('pullrequest_comments', comments_count),
135 'comments_raw': comments_count,
135 'comments_raw': comments_count,
136 'closed': pr.is_closed(),
136 'closed': pr.is_closed(),
137 })
137 })
138
138
139 data = ({
139 data = ({
140 'draw': draw,
140 'draw': draw,
141 'data': data,
141 'data': data,
142 'recordsTotal': pull_requests_total_count,
142 'recordsTotal': pull_requests_total_count,
143 'recordsFiltered': pull_requests_total_count,
143 'recordsFiltered': pull_requests_total_count,
144 })
144 })
145 return data
145 return data
146
146
147 @LoginRequired()
147 @LoginRequired()
148 @HasRepoPermissionAnyDecorator(
148 @HasRepoPermissionAnyDecorator(
149 'repository.read', 'repository.write', 'repository.admin')
149 'repository.read', 'repository.write', 'repository.admin')
150 def pull_request_list(self):
150 def pull_request_list(self):
151 c = self.load_default_context()
151 c = self.load_default_context()
152
152
153 req_get = self.request.GET
153 req_get = self.request.GET
154 c.source = str2bool(req_get.get('source'))
154 c.source = str2bool(req_get.get('source'))
155 c.closed = str2bool(req_get.get('closed'))
155 c.closed = str2bool(req_get.get('closed'))
156 c.my = str2bool(req_get.get('my'))
156 c.my = str2bool(req_get.get('my'))
157 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
157 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
158 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159
159
160 c.active = 'open'
160 c.active = 'open'
161 if c.my:
161 if c.my:
162 c.active = 'my'
162 c.active = 'my'
163 if c.closed:
163 if c.closed:
164 c.active = 'closed'
164 c.active = 'closed'
165 if c.awaiting_review and not c.source:
165 if c.awaiting_review and not c.source:
166 c.active = 'awaiting'
166 c.active = 'awaiting'
167 if c.source and not c.awaiting_review:
167 if c.source and not c.awaiting_review:
168 c.active = 'source'
168 c.active = 'source'
169 if c.awaiting_my_review:
169 if c.awaiting_my_review:
170 c.active = 'awaiting_my'
170 c.active = 'awaiting_my'
171
171
172 return self._get_template_context(c)
172 return self._get_template_context(c)
173
173
174 @LoginRequired()
174 @LoginRequired()
175 @HasRepoPermissionAnyDecorator(
175 @HasRepoPermissionAnyDecorator(
176 'repository.read', 'repository.write', 'repository.admin')
176 'repository.read', 'repository.write', 'repository.admin')
177 def pull_request_list_data(self):
177 def pull_request_list_data(self):
178 self.load_default_context()
178 self.load_default_context()
179
179
180 # additional filters
180 # additional filters
181 req_get = self.request.GET
181 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
182 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
183 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
184 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
187
188 filter_type = 'awaiting_review' if awaiting_review \
188 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
189 else 'awaiting_my_review' if awaiting_my_review \
190 else None
190 else None
191
191
192 opened_by = None
192 opened_by = None
193 if my:
193 if my:
194 opened_by = [self._rhodecode_user.user_id]
194 opened_by = [self._rhodecode_user.user_id]
195
195
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
197 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
198 statuses = [PullRequest.STATUS_CLOSED]
199
199
200 data = self._get_pull_requests_list(
200 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
201 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
203
204 return data
204 return data
205
205
206 def _is_diff_cache_enabled(self, target_repo):
206 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
207 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
208 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
209 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
210 return caching_enabled
211
211
212 def _get_diffset(self, source_repo_name, source_repo,
212 def _get_diffset(self, source_repo_name, source_repo,
213 ancestor_commit,
213 ancestor_commit,
214 source_ref_id, target_ref_id,
214 source_ref_id, target_ref_id,
215 target_commit, source_commit, diff_limit, file_limit,
215 target_commit, source_commit, diff_limit, file_limit,
216 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
216 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217
217
218 target_commit_final = target_commit
218 target_commit_final = target_commit
219 source_commit_final = source_commit
219 source_commit_final = source_commit
220
220
221 if use_ancestor:
221 if use_ancestor:
222 # we might want to not use it for versions
222 # we might want to not use it for versions
223 target_ref_id = ancestor_commit.raw_id
223 target_ref_id = ancestor_commit.raw_id
224 target_commit_final = ancestor_commit
224 target_commit_final = ancestor_commit
225
225
226 vcs_diff = PullRequestModel().get_diff(
226 vcs_diff = PullRequestModel().get_diff(
227 source_repo, source_ref_id, target_ref_id,
227 source_repo, source_ref_id, target_ref_id,
228 hide_whitespace_changes, diff_context)
228 hide_whitespace_changes, diff_context)
229
229
230 diff_processor = diffs.DiffProcessor(
230 diff_processor = diffs.DiffProcessor(
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 file_limit=file_limit, show_full_diff=fulldiff)
232 file_limit=file_limit, show_full_diff=fulldiff)
233
233
234 _parsed = diff_processor.prepare()
234 _parsed = diff_processor.prepare()
235
235
236 diffset = codeblocks.DiffSet(
236 diffset = codeblocks.DiffSet(
237 repo_name=self.db_repo_name,
237 repo_name=self.db_repo_name,
238 source_repo_name=source_repo_name,
238 source_repo_name=source_repo_name,
239 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
239 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
241 )
241 )
242 diffset = self.path_filter.render_patchset_filtered(
242 diffset = self.path_filter.render_patchset_filtered(
243 diffset, _parsed, target_ref_id, source_ref_id)
243 diffset, _parsed, target_ref_id, source_ref_id)
244
244
245 return diffset
245 return diffset
246
246
247 def _get_range_diffset(self, source_scm, source_repo,
247 def _get_range_diffset(self, source_scm, source_repo,
248 commit1, commit2, diff_limit, file_limit,
248 commit1, commit2, diff_limit, file_limit,
249 fulldiff, hide_whitespace_changes, diff_context):
249 fulldiff, hide_whitespace_changes, diff_context):
250 vcs_diff = source_scm.get_diff(
250 vcs_diff = source_scm.get_diff(
251 commit1, commit2,
251 commit1, commit2,
252 ignore_whitespace=hide_whitespace_changes,
252 ignore_whitespace=hide_whitespace_changes,
253 context=diff_context)
253 context=diff_context)
254
254
255 diff_processor = diffs.DiffProcessor(
255 diff_processor = diffs.DiffProcessor(
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 file_limit=file_limit, show_full_diff=fulldiff)
257 file_limit=file_limit, show_full_diff=fulldiff)
258
258
259 _parsed = diff_processor.prepare()
259 _parsed = diff_processor.prepare()
260
260
261 diffset = codeblocks.DiffSet(
261 diffset = codeblocks.DiffSet(
262 repo_name=source_repo.repo_name,
262 repo_name=source_repo.repo_name,
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
265
265
266 diffset = self.path_filter.render_patchset_filtered(
266 diffset = self.path_filter.render_patchset_filtered(
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268
268
269 return diffset
269 return diffset
270
270
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 comments_model = CommentsModel()
272 comments_model = CommentsModel()
273
273
274 # GENERAL COMMENTS with versions #
274 # GENERAL COMMENTS with versions #
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = q.order_by(ChangesetComment.comment_id.asc())
276 q = q.order_by(ChangesetComment.comment_id.asc())
277 if not include_drafts:
277 if not include_drafts:
278 q = q.filter(ChangesetComment.draft == false())
278 q = q.filter(ChangesetComment.draft == false())
279 general_comments = q
279 general_comments = q
280
280
281 # pick comments we want to render at current version
281 # pick comments we want to render at current version
282 c.comment_versions = comments_model.aggregate_comments(
282 c.comment_versions = comments_model.aggregate_comments(
283 general_comments, versions, c.at_version_num)
283 general_comments, versions, c.at_version_num)
284
284
285 # INLINE COMMENTS with versions #
285 # INLINE COMMENTS with versions #
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 q = q.order_by(ChangesetComment.comment_id.asc())
287 q = q.order_by(ChangesetComment.comment_id.asc())
288 if not include_drafts:
288 if not include_drafts:
289 q = q.filter(ChangesetComment.draft == false())
289 q = q.filter(ChangesetComment.draft == false())
290 inline_comments = q
290 inline_comments = q
291
291
292 c.inline_versions = comments_model.aggregate_comments(
292 c.inline_versions = comments_model.aggregate_comments(
293 inline_comments, versions, c.at_version_num, inline=True)
293 inline_comments, versions, c.at_version_num, inline=True)
294
294
295 # Comments inline+general
295 # Comments inline+general
296 if c.at_version:
296 if c.at_version:
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
299 else:
299 else:
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
302
302
303 return general_comments, inline_comments
303 return general_comments, inline_comments
304
304
305 @LoginRequired()
305 @LoginRequired()
306 @HasRepoPermissionAnyDecorator(
306 @HasRepoPermissionAnyDecorator(
307 'repository.read', 'repository.write', 'repository.admin')
307 'repository.read', 'repository.write', 'repository.admin')
308 def pull_request_show(self):
308 def pull_request_show(self):
309 _ = self.request.translate
309 _ = self.request.translate
310 c = self.load_default_context()
310 c = self.load_default_context()
311
311
312 pull_request = PullRequest.get_or_404(
312 pull_request = PullRequest.get_or_404(
313 self.request.matchdict['pull_request_id'])
313 self.request.matchdict['pull_request_id'])
314 pull_request_id = pull_request.pull_request_id
314 pull_request_id = pull_request.pull_request_id
315
315
316 c.state_progressing = pull_request.is_state_changing()
316 c.state_progressing = pull_request.is_state_changing()
317 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
317 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
318
318
319 _new_state = {
319 _new_state = {
320 'created': PullRequest.STATE_CREATED,
320 'created': PullRequest.STATE_CREATED,
321 }.get(self.request.GET.get('force_state'))
321 }.get(self.request.GET.get('force_state'))
322
322
323 if c.is_super_admin and _new_state:
323 if c.is_super_admin and _new_state:
324 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
324 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
325 h.flash(
325 h.flash(
326 _('Pull Request state was force changed to `{}`').format(_new_state),
326 _('Pull Request state was force changed to `{}`').format(_new_state),
327 category='success')
327 category='success')
328 Session().commit()
328 Session().commit()
329
329
330 raise HTTPFound(h.route_path(
330 raise HTTPFound(h.route_path(
331 'pullrequest_show', repo_name=self.db_repo_name,
331 'pullrequest_show', repo_name=self.db_repo_name,
332 pull_request_id=pull_request_id))
332 pull_request_id=pull_request_id))
333
333
334 version = self.request.GET.get('version')
334 version = self.request.GET.get('version')
335 from_version = self.request.GET.get('from_version') or version
335 from_version = self.request.GET.get('from_version') or version
336 merge_checks = self.request.GET.get('merge_checks')
336 merge_checks = self.request.GET.get('merge_checks')
337 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
337 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
338 force_refresh = str2bool(self.request.GET.get('force_refresh'))
338 force_refresh = str2bool(self.request.GET.get('force_refresh'))
339 c.range_diff_on = self.request.GET.get('range-diff') == "1"
339 c.range_diff_on = self.request.GET.get('range-diff') == "1"
340
340
341 # fetch global flags of ignore ws or context lines
341 # fetch global flags of ignore ws or context lines
342 diff_context = diffs.get_diff_context(self.request)
342 diff_context = diffs.get_diff_context(self.request)
343 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
343 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
344
344
345 (pull_request_latest,
345 (pull_request_latest,
346 pull_request_at_ver,
346 pull_request_at_ver,
347 pull_request_display_obj,
347 pull_request_display_obj,
348 at_version) = PullRequestModel().get_pr_version(
348 at_version) = PullRequestModel().get_pr_version(
349 pull_request_id, version=version)
349 pull_request_id, version=version)
350
350
351 pr_closed = pull_request_latest.is_closed()
351 pr_closed = pull_request_latest.is_closed()
352
352
353 if pr_closed and (version or from_version):
353 if pr_closed and (version or from_version):
354 # not allow to browse versions for closed PR
354 # not allow to browse versions for closed PR
355 raise HTTPFound(h.route_path(
355 raise HTTPFound(h.route_path(
356 'pullrequest_show', repo_name=self.db_repo_name,
356 'pullrequest_show', repo_name=self.db_repo_name,
357 pull_request_id=pull_request_id))
357 pull_request_id=pull_request_id))
358
358
359 versions = pull_request_display_obj.versions()
359 versions = pull_request_display_obj.versions()
360
360
361 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
361 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
362
362
363 # used to store per-commit range diffs
363 # used to store per-commit range diffs
364 c.changes = collections.OrderedDict()
364 c.changes = collections.OrderedDict()
365
365
366 c.at_version = at_version
366 c.at_version = at_version
367 c.at_version_num = (at_version
367 c.at_version_num = (at_version
368 if at_version and at_version != PullRequest.LATEST_VER
368 if at_version and at_version != PullRequest.LATEST_VER
369 else None)
369 else None)
370
370
371 c.at_version_index = ChangesetComment.get_index_from_version(
371 c.at_version_index = ChangesetComment.get_index_from_version(
372 c.at_version_num, versions)
372 c.at_version_num, versions)
373
373
374 (prev_pull_request_latest,
374 (prev_pull_request_latest,
375 prev_pull_request_at_ver,
375 prev_pull_request_at_ver,
376 prev_pull_request_display_obj,
376 prev_pull_request_display_obj,
377 prev_at_version) = PullRequestModel().get_pr_version(
377 prev_at_version) = PullRequestModel().get_pr_version(
378 pull_request_id, version=from_version)
378 pull_request_id, version=from_version)
379
379
380 c.from_version = prev_at_version
380 c.from_version = prev_at_version
381 c.from_version_num = (prev_at_version
381 c.from_version_num = (prev_at_version
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 else None)
383 else None)
384 c.from_version_index = ChangesetComment.get_index_from_version(
384 c.from_version_index = ChangesetComment.get_index_from_version(
385 c.from_version_num, versions)
385 c.from_version_num, versions)
386
386
387 # define if we're in COMPARE mode or VIEW at version mode
387 # define if we're in COMPARE mode or VIEW at version mode
388 compare = at_version != prev_at_version
388 compare = at_version != prev_at_version
389
389
390 # pull_requests repo_name we opened it against
390 # pull_requests repo_name we opened it against
391 # ie. target_repo must match
391 # ie. target_repo must match
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 log.warning('Mismatch between the current repo: %s, and target %s',
393 log.warning('Mismatch between the current repo: %s, and target %s',
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 raise HTTPNotFound()
395 raise HTTPNotFound()
396
396
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398
398
399 c.pull_request = pull_request_display_obj
399 c.pull_request = pull_request_display_obj
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 c.pull_request_latest = pull_request_latest
401 c.pull_request_latest = pull_request_latest
402
402
403 # inject latest version
403 # inject latest version
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 c.versions = versions + [latest_ver]
405 c.versions = versions + [latest_ver]
406
406
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 c.allowed_to_change_status = False
408 c.allowed_to_change_status = False
409 c.allowed_to_update = False
409 c.allowed_to_update = False
410 c.allowed_to_merge = False
410 c.allowed_to_merge = False
411 c.allowed_to_delete = False
411 c.allowed_to_delete = False
412 c.allowed_to_comment = False
412 c.allowed_to_comment = False
413 c.allowed_to_close = False
413 c.allowed_to_close = False
414 else:
414 else:
415 can_change_status = PullRequestModel().check_user_change_status(
415 can_change_status = PullRequestModel().check_user_change_status(
416 pull_request_at_ver, self._rhodecode_user)
416 pull_request_at_ver, self._rhodecode_user)
417 c.allowed_to_change_status = can_change_status and not pr_closed
417 c.allowed_to_change_status = can_change_status and not pr_closed
418
418
419 c.allowed_to_update = PullRequestModel().check_user_update(
419 c.allowed_to_update = PullRequestModel().check_user_update(
420 pull_request_latest, self._rhodecode_user) and not pr_closed
420 pull_request_latest, self._rhodecode_user) and not pr_closed
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 pull_request_latest, self._rhodecode_user) and not pr_closed
422 pull_request_latest, self._rhodecode_user) and not pr_closed
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 pull_request_latest, self._rhodecode_user) and not pr_closed
424 pull_request_latest, self._rhodecode_user) and not pr_closed
425 c.allowed_to_comment = not pr_closed
425 c.allowed_to_comment = not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427
427
428 c.forbid_adding_reviewers = False
428 c.forbid_adding_reviewers = False
429
429
430 if pull_request_latest.reviewer_data and \
430 if pull_request_latest.reviewer_data and \
431 'rules' in pull_request_latest.reviewer_data:
431 'rules' in pull_request_latest.reviewer_data:
432 rules = pull_request_latest.reviewer_data['rules'] or {}
432 rules = pull_request_latest.reviewer_data['rules'] or {}
433 try:
433 try:
434 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
434 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
435 except Exception:
435 except Exception:
436 pass
436 pass
437
437
438 # check merge capabilities
438 # check merge capabilities
439 _merge_check = MergeCheck.validate(
439 _merge_check = MergeCheck.validate(
440 pull_request_latest, auth_user=self._rhodecode_user,
440 pull_request_latest, auth_user=self._rhodecode_user,
441 translator=self.request.translate,
441 translator=self.request.translate,
442 force_shadow_repo_refresh=force_refresh)
442 force_shadow_repo_refresh=force_refresh)
443
443
444 c.pr_merge_errors = _merge_check.error_details
444 c.pr_merge_errors = _merge_check.error_details
445 c.pr_merge_possible = not _merge_check.failed
445 c.pr_merge_possible = not _merge_check.failed
446 c.pr_merge_message = _merge_check.merge_msg
446 c.pr_merge_message = _merge_check.merge_msg
447 c.pr_merge_source_commit = _merge_check.source_commit
447 c.pr_merge_source_commit = _merge_check.source_commit
448 c.pr_merge_target_commit = _merge_check.target_commit
448 c.pr_merge_target_commit = _merge_check.target_commit
449
449
450 c.pr_merge_info = MergeCheck.get_merge_conditions(
450 c.pr_merge_info = MergeCheck.get_merge_conditions(
451 pull_request_latest, translator=self.request.translate)
451 pull_request_latest, translator=self.request.translate)
452
452
453 c.pull_request_review_status = _merge_check.review_status
453 c.pull_request_review_status = _merge_check.review_status
454 if merge_checks:
454 if merge_checks:
455 self.request.override_renderer = \
455 self.request.override_renderer = \
456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
457 return self._get_template_context(c)
457 return self._get_template_context(c)
458
458
459 c.reviewers_count = pull_request.reviewers_count
459 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
460 c.observers_count = pull_request.observers_count
461
461
462 # reviewers and statuses
462 # reviewers and statuses
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
466
466
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
468 member_reviewer = h.reviewer_as_json(
468 member_reviewer = h.reviewer_as_json(
469 member, reasons=reasons, mandatory=mandatory,
469 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
470 role=review_obj.role,
471 user_group=review_obj.rule_user_group_data()
471 user_group=review_obj.rule_user_group_data()
472 )
472 )
473
473
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
475 member_reviewer['review_status'] = current_review_status
475 member_reviewer['review_status'] = current_review_status
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
479
479
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
481
481
482 for observer_obj, member in pull_request_at_ver.observers():
482 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
483 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
484 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
485 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
486 user_group=observer_obj.rule_user_group_data()
487 )
487 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
488 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
490
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
492
493 general_comments, inline_comments = \
493 general_comments, inline_comments = \
494 self.register_comments_vars(c, pull_request_latest, versions)
494 self.register_comments_vars(c, pull_request_latest, versions)
495
495
496 # TODOs
496 # TODOs
497 c.unresolved_comments = CommentsModel() \
497 c.unresolved_comments = CommentsModel() \
498 .get_pull_request_unresolved_todos(pull_request_latest)
498 .get_pull_request_unresolved_todos(pull_request_latest)
499 c.resolved_comments = CommentsModel() \
499 c.resolved_comments = CommentsModel() \
500 .get_pull_request_resolved_todos(pull_request_latest)
500 .get_pull_request_resolved_todos(pull_request_latest)
501
501
502 # Drafts
502 # Drafts
503 c.draft_comments = CommentsModel().get_pull_request_drafts(
503 c.draft_comments = CommentsModel().get_pull_request_drafts(
504 self._rhodecode_db_user.user_id,
504 self._rhodecode_db_user.user_id,
505 pull_request_latest)
505 pull_request_latest)
506
506
507 # if we use version, then do not show later comments
507 # if we use version, then do not show later comments
508 # than current version
508 # than current version
509 display_inline_comments = collections.defaultdict(
509 display_inline_comments = collections.defaultdict(
510 lambda: collections.defaultdict(list))
510 lambda: collections.defaultdict(list))
511 for co in inline_comments:
511 for co in inline_comments:
512 if c.at_version_num:
512 if c.at_version_num:
513 # pick comments that are at least UPTO given version, so we
513 # pick comments that are at least UPTO given version, so we
514 # don't render comments for higher version
514 # don't render comments for higher version
515 should_render = co.pull_request_version_id and \
515 should_render = co.pull_request_version_id and \
516 co.pull_request_version_id <= c.at_version_num
516 co.pull_request_version_id <= c.at_version_num
517 else:
517 else:
518 # showing all, for 'latest'
518 # showing all, for 'latest'
519 should_render = True
519 should_render = True
520
520
521 if should_render:
521 if should_render:
522 display_inline_comments[co.f_path][co.line_no].append(co)
522 display_inline_comments[co.f_path][co.line_no].append(co)
523
523
524 # load diff data into template context, if we use compare mode then
524 # load diff data into template context, if we use compare mode then
525 # diff is calculated based on changes between versions of PR
525 # diff is calculated based on changes between versions of PR
526
526
527 source_repo = pull_request_at_ver.source_repo
527 source_repo = pull_request_at_ver.source_repo
528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
529
529
530 target_repo = pull_request_at_ver.target_repo
530 target_repo = pull_request_at_ver.target_repo
531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
532
532
533 if compare:
533 if compare:
534 # in compare switch the diff base to latest commit from prev version
534 # in compare switch the diff base to latest commit from prev version
535 target_ref_id = prev_pull_request_display_obj.revisions[0]
535 target_ref_id = prev_pull_request_display_obj.revisions[0]
536
536
537 # despite opening commits for bookmarks/branches/tags, we always
537 # despite opening commits for bookmarks/branches/tags, we always
538 # convert this to rev to prevent changes after bookmark or branch change
538 # convert this to rev to prevent changes after bookmark or branch change
539 c.source_ref_type = 'rev'
539 c.source_ref_type = 'rev'
540 c.source_ref = source_ref_id
540 c.source_ref = source_ref_id
541
541
542 c.target_ref_type = 'rev'
542 c.target_ref_type = 'rev'
543 c.target_ref = target_ref_id
543 c.target_ref = target_ref_id
544
544
545 c.source_repo = source_repo
545 c.source_repo = source_repo
546 c.target_repo = target_repo
546 c.target_repo = target_repo
547
547
548 c.commit_ranges = []
548 c.commit_ranges = []
549 source_commit = EmptyCommit()
549 source_commit = EmptyCommit()
550 target_commit = EmptyCommit()
550 target_commit = EmptyCommit()
551 c.missing_requirements = False
551 c.missing_requirements = False
552
552
553 source_scm = source_repo.scm_instance()
553 source_scm = source_repo.scm_instance()
554 target_scm = target_repo.scm_instance()
554 target_scm = target_repo.scm_instance()
555
555
556 shadow_scm = None
556 shadow_scm = None
557 try:
557 try:
558 shadow_scm = pull_request_latest.get_shadow_repo()
558 shadow_scm = pull_request_latest.get_shadow_repo()
559 except Exception:
559 except Exception:
560 log.debug('Failed to get shadow repo', exc_info=True)
560 log.debug('Failed to get shadow repo', exc_info=True)
561 # try first the existing source_repo, and then shadow
561 # try first the existing source_repo, and then shadow
562 # repo if we can obtain one
562 # repo if we can obtain one
563 commits_source_repo = source_scm
563 commits_source_repo = source_scm
564 if shadow_scm:
564 if shadow_scm:
565 commits_source_repo = shadow_scm
565 commits_source_repo = shadow_scm
566
566
567 c.commits_source_repo = commits_source_repo
567 c.commits_source_repo = commits_source_repo
568 c.ancestor = None # set it to None, to hide it from PR view
568 c.ancestor = None # set it to None, to hide it from PR view
569
569
570 # empty version means latest, so we keep this to prevent
570 # empty version means latest, so we keep this to prevent
571 # double caching
571 # double caching
572 version_normalized = version or PullRequest.LATEST_VER
572 version_normalized = version or PullRequest.LATEST_VER
573 from_version_normalized = from_version or PullRequest.LATEST_VER
573 from_version_normalized = from_version or PullRequest.LATEST_VER
574
574
575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
576 cache_file_path = diff_cache_exist(
576 cache_file_path = diff_cache_exist(
577 cache_path, 'pull_request', pull_request_id, version_normalized,
577 cache_path, 'pull_request', pull_request_id, version_normalized,
578 from_version_normalized, source_ref_id, target_ref_id,
578 from_version_normalized, source_ref_id, target_ref_id,
579 hide_whitespace_changes, diff_context, c.fulldiff)
579 hide_whitespace_changes, diff_context, c.fulldiff)
580
580
581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
582 force_recache = self.get_recache_flag()
582 force_recache = self.get_recache_flag()
583
583
584 cached_diff = None
584 cached_diff = None
585 if caching_enabled:
585 if caching_enabled:
586 cached_diff = load_cached_diff(cache_file_path)
586 cached_diff = load_cached_diff(cache_file_path)
587
587
588 has_proper_commit_cache = (
588 has_proper_commit_cache = (
589 cached_diff and cached_diff.get('commits')
589 cached_diff and cached_diff.get('commits')
590 and len(cached_diff.get('commits', [])) == 5
590 and len(cached_diff.get('commits', [])) == 5
591 and cached_diff.get('commits')[0]
591 and cached_diff.get('commits')[0]
592 and cached_diff.get('commits')[3])
592 and cached_diff.get('commits')[3])
593
593
594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
595 diff_commit_cache = \
595 diff_commit_cache = \
596 (ancestor_commit, commit_cache, missing_requirements,
596 (ancestor_commit, commit_cache, missing_requirements,
597 source_commit, target_commit) = cached_diff['commits']
597 source_commit, target_commit) = cached_diff['commits']
598 else:
598 else:
599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
600 # merge errors resulting in potentially hidden commits in the shadow repo.
600 # merge errors resulting in potentially hidden commits in the shadow repo.
601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
602 and _merge_check.merge_response
602 and _merge_check.merge_response
603 maybe_unreachable = maybe_unreachable \
603 maybe_unreachable = maybe_unreachable \
604 and _merge_check.merge_response.metadata.get('unresolved_files')
604 and _merge_check.merge_response.metadata.get('unresolved_files')
605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
606 diff_commit_cache = \
606 diff_commit_cache = \
607 (ancestor_commit, commit_cache, missing_requirements,
607 (ancestor_commit, commit_cache, missing_requirements,
608 source_commit, target_commit) = self.get_commits(
608 source_commit, target_commit) = self.get_commits(
609 commits_source_repo,
609 commits_source_repo,
610 pull_request_at_ver,
610 pull_request_at_ver,
611 source_commit,
611 source_commit,
612 source_ref_id,
612 source_ref_id,
613 source_scm,
613 source_scm,
614 target_commit,
614 target_commit,
615 target_ref_id,
615 target_ref_id,
616 target_scm,
616 target_scm,
617 maybe_unreachable=maybe_unreachable)
617 maybe_unreachable=maybe_unreachable)
618
618
619 # register our commit range
619 # register our commit range
620 for comm in commit_cache.values():
620 for comm in commit_cache.values():
621 c.commit_ranges.append(comm)
621 c.commit_ranges.append(comm)
622
622
623 c.missing_requirements = missing_requirements
623 c.missing_requirements = missing_requirements
624 c.ancestor_commit = ancestor_commit
624 c.ancestor_commit = ancestor_commit
625 c.statuses = source_repo.statuses(
625 c.statuses = source_repo.statuses(
626 [x.raw_id for x in c.commit_ranges])
626 [x.raw_id for x in c.commit_ranges])
627
627
628 # auto collapse if we have more than limit
628 # auto collapse if we have more than limit
629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
631 c.compare_mode = compare
631 c.compare_mode = compare
632
632
633 # diff_limit is the old behavior, will cut off the whole diff
633 # diff_limit is the old behavior, will cut off the whole diff
634 # if the limit is applied otherwise will just hide the
634 # if the limit is applied otherwise will just hide the
635 # big files from the front-end
635 # big files from the front-end
636 diff_limit = c.visual.cut_off_limit_diff
636 diff_limit = c.visual.cut_off_limit_diff
637 file_limit = c.visual.cut_off_limit_file
637 file_limit = c.visual.cut_off_limit_file
638
638
639 c.missing_commits = False
639 c.missing_commits = False
640 if (c.missing_requirements
640 if (c.missing_requirements
641 or isinstance(source_commit, EmptyCommit)
641 or isinstance(source_commit, EmptyCommit)
642 or source_commit == target_commit):
642 or source_commit == target_commit):
643
643
644 c.missing_commits = True
644 c.missing_commits = True
645 else:
645 else:
646 c.inline_comments = display_inline_comments
646 c.inline_comments = display_inline_comments
647
647
648 use_ancestor = True
648 use_ancestor = True
649 if from_version_normalized != version_normalized:
649 if from_version_normalized != version_normalized:
650 use_ancestor = False
650 use_ancestor = False
651
651
652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
653 if not force_recache and has_proper_diff_cache:
653 if not force_recache and has_proper_diff_cache:
654 c.diffset = cached_diff['diff']
654 c.diffset = cached_diff['diff']
655 else:
655 else:
656 try:
656 try:
657 c.diffset = self._get_diffset(
657 c.diffset = self._get_diffset(
658 c.source_repo.repo_name, commits_source_repo,
658 c.source_repo.repo_name, commits_source_repo,
659 c.ancestor_commit,
659 c.ancestor_commit,
660 source_ref_id, target_ref_id,
660 source_ref_id, target_ref_id,
661 target_commit, source_commit,
661 target_commit, source_commit,
662 diff_limit, file_limit, c.fulldiff,
662 diff_limit, file_limit, c.fulldiff,
663 hide_whitespace_changes, diff_context,
663 hide_whitespace_changes, diff_context,
664 use_ancestor=use_ancestor
664 use_ancestor=use_ancestor
665 )
665 )
666
666
667 # save cached diff
667 # save cached diff
668 if caching_enabled:
668 if caching_enabled:
669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
670 except CommitDoesNotExistError:
670 except CommitDoesNotExistError:
671 log.exception('Failed to generate diffset')
671 log.exception('Failed to generate diffset')
672 c.missing_commits = True
672 c.missing_commits = True
673
673
674 if not c.missing_commits:
674 if not c.missing_commits:
675
675
676 c.limited_diff = c.diffset.limited_diff
676 c.limited_diff = c.diffset.limited_diff
677
677
678 # calculate removed files that are bound to comments
678 # calculate removed files that are bound to comments
679 comment_deleted_files = [
679 comment_deleted_files = [
680 fname for fname in display_inline_comments
680 fname for fname in display_inline_comments
681 if fname not in c.diffset.file_stats]
681 if fname not in c.diffset.file_stats]
682
682
683 c.deleted_files_comments = collections.defaultdict(dict)
683 c.deleted_files_comments = collections.defaultdict(dict)
684 for fname, per_line_comments in display_inline_comments.items():
684 for fname, per_line_comments in display_inline_comments.items():
685 if fname in comment_deleted_files:
685 if fname in comment_deleted_files:
686 c.deleted_files_comments[fname]['stats'] = 0
686 c.deleted_files_comments[fname]['stats'] = 0
687 c.deleted_files_comments[fname]['comments'] = list()
687 c.deleted_files_comments[fname]['comments'] = list()
688 for lno, comments in per_line_comments.items():
688 for lno, comments in per_line_comments.items():
689 c.deleted_files_comments[fname]['comments'].extend(comments)
689 c.deleted_files_comments[fname]['comments'].extend(comments)
690
690
691 # maybe calculate the range diff
691 # maybe calculate the range diff
692 if c.range_diff_on:
692 if c.range_diff_on:
693 # TODO(marcink): set whitespace/context
693 # TODO(marcink): set whitespace/context
694 context_lcl = 3
694 context_lcl = 3
695 ign_whitespace_lcl = False
695 ign_whitespace_lcl = False
696
696
697 for commit in c.commit_ranges:
697 for commit in c.commit_ranges:
698 commit2 = commit
698 commit2 = commit
699 commit1 = commit.first_parent
699 commit1 = commit.first_parent
700
700
701 range_diff_cache_file_path = diff_cache_exist(
701 range_diff_cache_file_path = diff_cache_exist(
702 cache_path, 'diff', commit.raw_id,
702 cache_path, 'diff', commit.raw_id,
703 ign_whitespace_lcl, context_lcl, c.fulldiff)
703 ign_whitespace_lcl, context_lcl, c.fulldiff)
704
704
705 cached_diff = None
705 cached_diff = None
706 if caching_enabled:
706 if caching_enabled:
707 cached_diff = load_cached_diff(range_diff_cache_file_path)
707 cached_diff = load_cached_diff(range_diff_cache_file_path)
708
708
709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
710 if not force_recache and has_proper_diff_cache:
710 if not force_recache and has_proper_diff_cache:
711 diffset = cached_diff['diff']
711 diffset = cached_diff['diff']
712 else:
712 else:
713 diffset = self._get_range_diffset(
713 diffset = self._get_range_diffset(
714 commits_source_repo, source_repo,
714 commits_source_repo, source_repo,
715 commit1, commit2, diff_limit, file_limit,
715 commit1, commit2, diff_limit, file_limit,
716 c.fulldiff, ign_whitespace_lcl, context_lcl
716 c.fulldiff, ign_whitespace_lcl, context_lcl
717 )
717 )
718
718
719 # save cached diff
719 # save cached diff
720 if caching_enabled:
720 if caching_enabled:
721 cache_diff(range_diff_cache_file_path, diffset, None)
721 cache_diff(range_diff_cache_file_path, diffset, None)
722
722
723 c.changes[commit.raw_id] = diffset
723 c.changes[commit.raw_id] = diffset
724
724
725 # this is a hack to properly display links, when creating PR, the
725 # this is a hack to properly display links, when creating PR, the
726 # compare view and others uses different notation, and
726 # compare view and others uses different notation, and
727 # compare_commits.mako renders links based on the target_repo.
727 # compare_commits.mako renders links based on the target_repo.
728 # We need to swap that here to generate it properly on the html side
728 # We need to swap that here to generate it properly on the html side
729 c.target_repo = c.source_repo
729 c.target_repo = c.source_repo
730
730
731 c.commit_statuses = ChangesetStatus.STATUSES
731 c.commit_statuses = ChangesetStatus.STATUSES
732
732
733 c.show_version_changes = not pr_closed
733 c.show_version_changes = not pr_closed
734 if c.show_version_changes:
734 if c.show_version_changes:
735 cur_obj = pull_request_at_ver
735 cur_obj = pull_request_at_ver
736 prev_obj = prev_pull_request_at_ver
736 prev_obj = prev_pull_request_at_ver
737
737
738 old_commit_ids = prev_obj.revisions
738 old_commit_ids = prev_obj.revisions
739 new_commit_ids = cur_obj.revisions
739 new_commit_ids = cur_obj.revisions
740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
741 old_commit_ids, new_commit_ids)
741 old_commit_ids, new_commit_ids)
742 c.commit_changes_summary = commit_changes
742 c.commit_changes_summary = commit_changes
743
743
744 # calculate the diff for commits between versions
744 # calculate the diff for commits between versions
745 c.commit_changes = []
745 c.commit_changes = []
746
746
747 def mark(cs, fw):
747 def mark(cs, fw):
748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
749
749
750 for c_type, raw_id in mark(commit_changes.added, 'a') \
750 for c_type, raw_id in mark(commit_changes.added, 'a') \
751 + mark(commit_changes.removed, 'r') \
751 + mark(commit_changes.removed, 'r') \
752 + mark(commit_changes.common, 'c'):
752 + mark(commit_changes.common, 'c'):
753
753
754 if raw_id in commit_cache:
754 if raw_id in commit_cache:
755 commit = commit_cache[raw_id]
755 commit = commit_cache[raw_id]
756 else:
756 else:
757 try:
757 try:
758 commit = commits_source_repo.get_commit(raw_id)
758 commit = commits_source_repo.get_commit(raw_id)
759 except CommitDoesNotExistError:
759 except CommitDoesNotExistError:
760 # in case we fail extracting still use "dummy" commit
760 # in case we fail extracting still use "dummy" commit
761 # for display in commit diff
761 # for display in commit diff
762 commit = h.AttributeDict(
762 commit = h.AttributeDict(
763 {'raw_id': raw_id,
763 {'raw_id': raw_id,
764 'message': 'EMPTY or MISSING COMMIT'})
764 'message': 'EMPTY or MISSING COMMIT'})
765 c.commit_changes.append([c_type, commit])
765 c.commit_changes.append([c_type, commit])
766
766
767 # current user review statuses for each version
767 # current user review statuses for each version
768 c.review_versions = {}
768 c.review_versions = {}
769 is_reviewer = PullRequestModel().is_user_reviewer(
769 is_reviewer = PullRequestModel().is_user_reviewer(
770 pull_request, self._rhodecode_user)
770 pull_request, self._rhodecode_user)
771 if is_reviewer:
771 if is_reviewer:
772 for co in general_comments:
772 for co in general_comments:
773 if co.author.user_id == self._rhodecode_user.user_id:
773 if co.author.user_id == self._rhodecode_user.user_id:
774 status = co.status_change
774 status = co.status_change
775 if status:
775 if status:
776 _ver_pr = status[0].comment.pull_request_version_id
776 _ver_pr = status[0].comment.pull_request_version_id
777 c.review_versions[_ver_pr] = status[0]
777 c.review_versions[_ver_pr] = status[0]
778
778
779 return self._get_template_context(c)
779 return self._get_template_context(c)
780
780
781 def get_commits(
781 def get_commits(
782 self, commits_source_repo, pull_request_at_ver, source_commit,
782 self, commits_source_repo, pull_request_at_ver, source_commit,
783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
784 maybe_unreachable=False):
784 maybe_unreachable=False):
785
785
786 commit_cache = collections.OrderedDict()
786 commit_cache = collections.OrderedDict()
787 missing_requirements = False
787 missing_requirements = False
788
788
789 try:
789 try:
790 pre_load = ["author", "date", "message", "branch", "parents"]
790 pre_load = ["author", "date", "message", "branch", "parents"]
791
791
792 pull_request_commits = pull_request_at_ver.revisions
792 pull_request_commits = pull_request_at_ver.revisions
793 log.debug('Loading %s commits from %s',
793 log.debug('Loading %s commits from %s',
794 len(pull_request_commits), commits_source_repo)
794 len(pull_request_commits), commits_source_repo)
795
795
796 for rev in pull_request_commits:
796 for rev in pull_request_commits:
797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
798 maybe_unreachable=maybe_unreachable)
798 maybe_unreachable=maybe_unreachable)
799 commit_cache[comm.raw_id] = comm
799 commit_cache[comm.raw_id] = comm
800
800
801 # Order here matters, we first need to get target, and then
801 # Order here matters, we first need to get target, and then
802 # the source
802 # the source
803 target_commit = commits_source_repo.get_commit(
803 target_commit = commits_source_repo.get_commit(
804 commit_id=safe_str(target_ref_id))
804 commit_id=safe_str(target_ref_id))
805
805
806 source_commit = commits_source_repo.get_commit(
806 source_commit = commits_source_repo.get_commit(
807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
808 except CommitDoesNotExistError:
808 except CommitDoesNotExistError:
809 log.warning('Failed to get commit from `{}` repo'.format(
809 log.warning('Failed to get commit from `{}` repo'.format(
810 commits_source_repo), exc_info=True)
810 commits_source_repo), exc_info=True)
811 except RepositoryRequirementError:
811 except RepositoryRequirementError:
812 log.warning('Failed to get all required data from repo', exc_info=True)
812 log.warning('Failed to get all required data from repo', exc_info=True)
813 missing_requirements = True
813 missing_requirements = True
814
814
815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
816
816
817 try:
817 try:
818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
819 except Exception:
819 except Exception:
820 ancestor_commit = None
820 ancestor_commit = None
821
821
822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
823
823
824 def assure_not_empty_repo(self):
824 def assure_not_empty_repo(self):
825 _ = self.request.translate
825 _ = self.request.translate
826
826
827 try:
827 try:
828 self.db_repo.scm_instance().get_commit()
828 self.db_repo.scm_instance().get_commit()
829 except EmptyRepositoryError:
829 except EmptyRepositoryError:
830 h.flash(h.literal(_('There are no commits yet')),
830 h.flash(h.literal(_('There are no commits yet')),
831 category='warning')
831 category='warning')
832 raise HTTPFound(
832 raise HTTPFound(
833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
834
834
835 @LoginRequired()
835 @LoginRequired()
836 @NotAnonymous()
836 @NotAnonymous()
837 @HasRepoPermissionAnyDecorator(
837 @HasRepoPermissionAnyDecorator(
838 'repository.read', 'repository.write', 'repository.admin')
838 'repository.read', 'repository.write', 'repository.admin')
839 def pull_request_new(self):
839 def pull_request_new(self):
840 _ = self.request.translate
840 _ = self.request.translate
841 c = self.load_default_context()
841 c = self.load_default_context()
842
842
843 self.assure_not_empty_repo()
843 self.assure_not_empty_repo()
844 source_repo = self.db_repo
844 source_repo = self.db_repo
845
845
846 commit_id = self.request.GET.get('commit')
846 commit_id = self.request.GET.get('commit')
847 branch_ref = self.request.GET.get('branch')
847 branch_ref = self.request.GET.get('branch')
848 bookmark_ref = self.request.GET.get('bookmark')
848 bookmark_ref = self.request.GET.get('bookmark')
849
849
850 try:
850 try:
851 source_repo_data = PullRequestModel().generate_repo_data(
851 source_repo_data = PullRequestModel().generate_repo_data(
852 source_repo, commit_id=commit_id,
852 source_repo, commit_id=commit_id,
853 branch=branch_ref, bookmark=bookmark_ref,
853 branch=branch_ref, bookmark=bookmark_ref,
854 translator=self.request.translate)
854 translator=self.request.translate)
855 except CommitDoesNotExistError as e:
855 except CommitDoesNotExistError as e:
856 log.exception(e)
856 log.exception(e)
857 h.flash(_('Commit does not exist'), 'error')
857 h.flash(_('Commit does not exist'), 'error')
858 raise HTTPFound(
858 raise HTTPFound(
859 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
859 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
860
860
861 default_target_repo = source_repo
861 default_target_repo = source_repo
862
862
863 if source_repo.parent and c.has_origin_repo_read_perm:
863 if source_repo.parent and c.has_origin_repo_read_perm:
864 parent_vcs_obj = source_repo.parent.scm_instance()
864 parent_vcs_obj = source_repo.parent.scm_instance()
865 if parent_vcs_obj and not parent_vcs_obj.is_empty():
865 if parent_vcs_obj and not parent_vcs_obj.is_empty():
866 # change default if we have a parent repo
866 # change default if we have a parent repo
867 default_target_repo = source_repo.parent
867 default_target_repo = source_repo.parent
868
868
869 target_repo_data = PullRequestModel().generate_repo_data(
869 target_repo_data = PullRequestModel().generate_repo_data(
870 default_target_repo, translator=self.request.translate)
870 default_target_repo, translator=self.request.translate)
871
871
872 selected_source_ref = source_repo_data['refs']['selected_ref']
872 selected_source_ref = source_repo_data['refs']['selected_ref']
873 title_source_ref = ''
873 title_source_ref = ''
874 if selected_source_ref:
874 if selected_source_ref:
875 title_source_ref = selected_source_ref.split(':', 2)[1]
875 title_source_ref = selected_source_ref.split(':', 2)[1]
876 c.default_title = PullRequestModel().generate_pullrequest_title(
876 c.default_title = PullRequestModel().generate_pullrequest_title(
877 source=source_repo.repo_name,
877 source=source_repo.repo_name,
878 source_ref=title_source_ref,
878 source_ref=title_source_ref,
879 target=default_target_repo.repo_name
879 target=default_target_repo.repo_name
880 )
880 )
881
881
882 c.default_repo_data = {
882 c.default_repo_data = {
883 'source_repo_name': source_repo.repo_name,
883 'source_repo_name': source_repo.repo_name,
884 'source_refs_json': json.dumps(source_repo_data),
884 'source_refs_json': json.dumps(source_repo_data),
885 'target_repo_name': default_target_repo.repo_name,
885 'target_repo_name': default_target_repo.repo_name,
886 'target_refs_json': json.dumps(target_repo_data),
886 'target_refs_json': json.dumps(target_repo_data),
887 }
887 }
888 c.default_source_ref = selected_source_ref
888 c.default_source_ref = selected_source_ref
889
889
890 return self._get_template_context(c)
890 return self._get_template_context(c)
891
891
892 @LoginRequired()
892 @LoginRequired()
893 @NotAnonymous()
893 @NotAnonymous()
894 @HasRepoPermissionAnyDecorator(
894 @HasRepoPermissionAnyDecorator(
895 'repository.read', 'repository.write', 'repository.admin')
895 'repository.read', 'repository.write', 'repository.admin')
896 def pull_request_repo_refs(self):
896 def pull_request_repo_refs(self):
897 self.load_default_context()
897 self.load_default_context()
898 target_repo_name = self.request.matchdict['target_repo_name']
898 target_repo_name = self.request.matchdict['target_repo_name']
899 repo = Repository.get_by_repo_name(target_repo_name)
899 repo = Repository.get_by_repo_name(target_repo_name)
900 if not repo:
900 if not repo:
901 raise HTTPNotFound()
901 raise HTTPNotFound()
902
902
903 target_perm = HasRepoPermissionAny(
903 target_perm = HasRepoPermissionAny(
904 'repository.read', 'repository.write', 'repository.admin')(
904 'repository.read', 'repository.write', 'repository.admin')(
905 target_repo_name)
905 target_repo_name)
906 if not target_perm:
906 if not target_perm:
907 raise HTTPNotFound()
907 raise HTTPNotFound()
908
908
909 return PullRequestModel().generate_repo_data(
909 return PullRequestModel().generate_repo_data(
910 repo, translator=self.request.translate)
910 repo, translator=self.request.translate)
911
911
912 @LoginRequired()
912 @LoginRequired()
913 @NotAnonymous()
913 @NotAnonymous()
914 @HasRepoPermissionAnyDecorator(
914 @HasRepoPermissionAnyDecorator(
915 'repository.read', 'repository.write', 'repository.admin')
915 'repository.read', 'repository.write', 'repository.admin')
916 def pullrequest_repo_targets(self):
916 def pullrequest_repo_targets(self):
917 _ = self.request.translate
917 _ = self.request.translate
918 filter_query = self.request.GET.get('query')
918 filter_query = self.request.GET.get('query')
919
919
920 # get the parents
920 # get the parents
921 parent_target_repos = []
921 parent_target_repos = []
922 if self.db_repo.parent:
922 if self.db_repo.parent:
923 parents_query = Repository.query() \
923 parents_query = Repository.query() \
924 .order_by(func.length(Repository.repo_name)) \
924 .order_by(func.length(Repository.repo_name)) \
925 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
925 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
926
926
927 if filter_query:
927 if filter_query:
928 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
928 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
929 parents_query = parents_query.filter(
929 parents_query = parents_query.filter(
930 Repository.repo_name.ilike(ilike_expression))
930 Repository.repo_name.ilike(ilike_expression))
931 parents = parents_query.limit(20).all()
931 parents = parents_query.limit(20).all()
932
932
933 for parent in parents:
933 for parent in parents:
934 parent_vcs_obj = parent.scm_instance()
934 parent_vcs_obj = parent.scm_instance()
935 if parent_vcs_obj and not parent_vcs_obj.is_empty():
935 if parent_vcs_obj and not parent_vcs_obj.is_empty():
936 parent_target_repos.append(parent)
936 parent_target_repos.append(parent)
937
937
938 # get other forks, and repo itself
938 # get other forks, and repo itself
939 query = Repository.query() \
939 query = Repository.query() \
940 .order_by(func.length(Repository.repo_name)) \
940 .order_by(func.length(Repository.repo_name)) \
941 .filter(
941 .filter(
942 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
942 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
943 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
943 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
944 ) \
944 ) \
945 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
945 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
946
946
947 if filter_query:
947 if filter_query:
948 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
948 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
949 query = query.filter(Repository.repo_name.ilike(ilike_expression))
949 query = query.filter(Repository.repo_name.ilike(ilike_expression))
950
950
951 limit = max(20 - len(parent_target_repos), 5) # not less then 5
951 limit = max(20 - len(parent_target_repos), 5) # not less then 5
952 target_repos = query.limit(limit).all()
952 target_repos = query.limit(limit).all()
953
953
954 all_target_repos = target_repos + parent_target_repos
954 all_target_repos = target_repos + parent_target_repos
955
955
956 repos = []
956 repos = []
957 # This checks permissions to the repositories
957 # This checks permissions to the repositories
958 for obj in ScmModel().get_repos(all_target_repos):
958 for obj in ScmModel().get_repos(all_target_repos):
959 repos.append({
959 repos.append({
960 'id': obj['name'],
960 'id': obj['name'],
961 'text': obj['name'],
961 'text': obj['name'],
962 'type': 'repo',
962 'type': 'repo',
963 'repo_id': obj['dbrepo']['repo_id'],
963 'repo_id': obj['dbrepo']['repo_id'],
964 'repo_type': obj['dbrepo']['repo_type'],
964 'repo_type': obj['dbrepo']['repo_type'],
965 'private': obj['dbrepo']['private'],
965 'private': obj['dbrepo']['private'],
966
966
967 })
967 })
968
968
969 data = {
969 data = {
970 'more': False,
970 'more': False,
971 'results': [{
971 'results': [{
972 'text': _('Repositories'),
972 'text': _('Repositories'),
973 'children': repos
973 'children': repos
974 }] if repos else []
974 }] if repos else []
975 }
975 }
976 return data
976 return data
977
977
978 @classmethod
978 @classmethod
979 def get_comment_ids(cls, post_data):
979 def get_comment_ids(cls, post_data):
980 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
980 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
981
981
982 @LoginRequired()
982 @LoginRequired()
983 @NotAnonymous()
983 @NotAnonymous()
984 @HasRepoPermissionAnyDecorator(
984 @HasRepoPermissionAnyDecorator(
985 'repository.read', 'repository.write', 'repository.admin')
985 'repository.read', 'repository.write', 'repository.admin')
986 def pullrequest_comments(self):
986 def pullrequest_comments(self):
987 self.load_default_context()
987 self.load_default_context()
988
988
989 pull_request = PullRequest.get_or_404(
989 pull_request = PullRequest.get_or_404(
990 self.request.matchdict['pull_request_id'])
990 self.request.matchdict['pull_request_id'])
991 pull_request_id = pull_request.pull_request_id
991 pull_request_id = pull_request.pull_request_id
992 version = self.request.GET.get('version')
992 version = self.request.GET.get('version')
993
993
994 _render = self.request.get_partial_renderer(
994 _render = self.request.get_partial_renderer(
995 'rhodecode:templates/base/sidebar.mako')
995 'rhodecode:templates/base/sidebar.mako')
996 c = _render.get_call_context()
996 c = _render.get_call_context()
997
997
998 (pull_request_latest,
998 (pull_request_latest,
999 pull_request_at_ver,
999 pull_request_at_ver,
1000 pull_request_display_obj,
1000 pull_request_display_obj,
1001 at_version) = PullRequestModel().get_pr_version(
1001 at_version) = PullRequestModel().get_pr_version(
1002 pull_request_id, version=version)
1002 pull_request_id, version=version)
1003 versions = pull_request_display_obj.versions()
1003 versions = pull_request_display_obj.versions()
1004 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1004 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1005 c.versions = versions + [latest_ver]
1005 c.versions = versions + [latest_ver]
1006
1006
1007 c.at_version = at_version
1007 c.at_version = at_version
1008 c.at_version_num = (at_version
1008 c.at_version_num = (at_version
1009 if at_version and at_version != PullRequest.LATEST_VER
1009 if at_version and at_version != PullRequest.LATEST_VER
1010 else None)
1010 else None)
1011
1011
1012 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1012 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1013 all_comments = c.inline_comments_flat + c.comments
1013 all_comments = c.inline_comments_flat + c.comments
1014
1014
1015 existing_ids = self.get_comment_ids(self.request.POST)
1015 existing_ids = self.get_comment_ids(self.request.POST)
1016 return _render('comments_table', all_comments, len(all_comments),
1016 return _render('comments_table', all_comments, len(all_comments),
1017 existing_ids=existing_ids)
1017 existing_ids=existing_ids)
1018
1018
1019 @LoginRequired()
1019 @LoginRequired()
1020 @NotAnonymous()
1020 @NotAnonymous()
1021 @HasRepoPermissionAnyDecorator(
1021 @HasRepoPermissionAnyDecorator(
1022 'repository.read', 'repository.write', 'repository.admin')
1022 'repository.read', 'repository.write', 'repository.admin')
1023 def pullrequest_todos(self):
1023 def pullrequest_todos(self):
1024 self.load_default_context()
1024 self.load_default_context()
1025
1025
1026 pull_request = PullRequest.get_or_404(
1026 pull_request = PullRequest.get_or_404(
1027 self.request.matchdict['pull_request_id'])
1027 self.request.matchdict['pull_request_id'])
1028 pull_request_id = pull_request.pull_request_id
1028 pull_request_id = pull_request.pull_request_id
1029 version = self.request.GET.get('version')
1029 version = self.request.GET.get('version')
1030
1030
1031 _render = self.request.get_partial_renderer(
1031 _render = self.request.get_partial_renderer(
1032 'rhodecode:templates/base/sidebar.mako')
1032 'rhodecode:templates/base/sidebar.mako')
1033 c = _render.get_call_context()
1033 c = _render.get_call_context()
1034 (pull_request_latest,
1034 (pull_request_latest,
1035 pull_request_at_ver,
1035 pull_request_at_ver,
1036 pull_request_display_obj,
1036 pull_request_display_obj,
1037 at_version) = PullRequestModel().get_pr_version(
1037 at_version) = PullRequestModel().get_pr_version(
1038 pull_request_id, version=version)
1038 pull_request_id, version=version)
1039 versions = pull_request_display_obj.versions()
1039 versions = pull_request_display_obj.versions()
1040 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1040 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1041 c.versions = versions + [latest_ver]
1041 c.versions = versions + [latest_ver]
1042
1042
1043 c.at_version = at_version
1043 c.at_version = at_version
1044 c.at_version_num = (at_version
1044 c.at_version_num = (at_version
1045 if at_version and at_version != PullRequest.LATEST_VER
1045 if at_version and at_version != PullRequest.LATEST_VER
1046 else None)
1046 else None)
1047
1047
1048 c.unresolved_comments = CommentsModel() \
1048 c.unresolved_comments = CommentsModel() \
1049 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1049 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1050 c.resolved_comments = CommentsModel() \
1050 c.resolved_comments = CommentsModel() \
1051 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1051 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1052
1052
1053 all_comments = c.unresolved_comments + c.resolved_comments
1053 all_comments = c.unresolved_comments + c.resolved_comments
1054 existing_ids = self.get_comment_ids(self.request.POST)
1054 existing_ids = self.get_comment_ids(self.request.POST)
1055 return _render('comments_table', all_comments, len(c.unresolved_comments),
1055 return _render('comments_table', all_comments, len(c.unresolved_comments),
1056 todo_comments=True, existing_ids=existing_ids)
1056 todo_comments=True, existing_ids=existing_ids)
1057
1057
1058 @LoginRequired()
1058 @LoginRequired()
1059 @NotAnonymous()
1059 @NotAnonymous()
1060 @HasRepoPermissionAnyDecorator(
1060 @HasRepoPermissionAnyDecorator(
1061 'repository.read', 'repository.write', 'repository.admin')
1061 'repository.read', 'repository.write', 'repository.admin')
1062 def pullrequest_drafts(self):
1062 def pullrequest_drafts(self):
1063 self.load_default_context()
1063 self.load_default_context()
1064
1064
1065 pull_request = PullRequest.get_or_404(
1065 pull_request = PullRequest.get_or_404(
1066 self.request.matchdict['pull_request_id'])
1066 self.request.matchdict['pull_request_id'])
1067 pull_request_id = pull_request.pull_request_id
1067 pull_request_id = pull_request.pull_request_id
1068 version = self.request.GET.get('version')
1068 version = self.request.GET.get('version')
1069
1069
1070 _render = self.request.get_partial_renderer(
1070 _render = self.request.get_partial_renderer(
1071 'rhodecode:templates/base/sidebar.mako')
1071 'rhodecode:templates/base/sidebar.mako')
1072 c = _render.get_call_context()
1072 c = _render.get_call_context()
1073
1073
1074 (pull_request_latest,
1074 (pull_request_latest,
1075 pull_request_at_ver,
1075 pull_request_at_ver,
1076 pull_request_display_obj,
1076 pull_request_display_obj,
1077 at_version) = PullRequestModel().get_pr_version(
1077 at_version) = PullRequestModel().get_pr_version(
1078 pull_request_id, version=version)
1078 pull_request_id, version=version)
1079 versions = pull_request_display_obj.versions()
1079 versions = pull_request_display_obj.versions()
1080 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1080 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1081 c.versions = versions + [latest_ver]
1081 c.versions = versions + [latest_ver]
1082
1082
1083 c.at_version = at_version
1083 c.at_version = at_version
1084 c.at_version_num = (at_version
1084 c.at_version_num = (at_version
1085 if at_version and at_version != PullRequest.LATEST_VER
1085 if at_version and at_version != PullRequest.LATEST_VER
1086 else None)
1086 else None)
1087
1087
1088 c.draft_comments = CommentsModel() \
1088 c.draft_comments = CommentsModel() \
1089 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1089 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1090
1090
1091 all_comments = c.draft_comments
1091 all_comments = c.draft_comments
1092
1092
1093 existing_ids = self.get_comment_ids(self.request.POST)
1093 existing_ids = self.get_comment_ids(self.request.POST)
1094 return _render('comments_table', all_comments, len(all_comments),
1094 return _render('comments_table', all_comments, len(all_comments),
1095 existing_ids=existing_ids, draft_comments=True)
1095 existing_ids=existing_ids, draft_comments=True)
1096
1096
1097 @LoginRequired()
1097 @LoginRequired()
1098 @NotAnonymous()
1098 @NotAnonymous()
1099 @HasRepoPermissionAnyDecorator(
1099 @HasRepoPermissionAnyDecorator(
1100 'repository.read', 'repository.write', 'repository.admin')
1100 'repository.read', 'repository.write', 'repository.admin')
1101 @CSRFRequired()
1101 @CSRFRequired()
1102 def pull_request_create(self):
1102 def pull_request_create(self):
1103 _ = self.request.translate
1103 _ = self.request.translate
1104 self.assure_not_empty_repo()
1104 self.assure_not_empty_repo()
1105 self.load_default_context()
1105 self.load_default_context()
1106
1106
1107 controls = peppercorn.parse(self.request.POST.items())
1107 controls = peppercorn.parse(self.request.POST.items())
1108
1108
1109 try:
1109 try:
1110 form = PullRequestForm(
1110 form = PullRequestForm(
1111 self.request.translate, self.db_repo.repo_id)()
1111 self.request.translate, self.db_repo.repo_id)()
1112 _form = form.to_python(controls)
1112 _form = form.to_python(controls)
1113 except formencode.Invalid as errors:
1113 except formencode.Invalid as errors:
1114 if errors.error_dict.get('revisions'):
1114 if errors.error_dict.get('revisions'):
1115 msg = 'Revisions: %s' % errors.error_dict['revisions']
1115 msg = 'Revisions: %s' % errors.error_dict['revisions']
1116 elif errors.error_dict.get('pullrequest_title'):
1116 elif errors.error_dict.get('pullrequest_title'):
1117 msg = errors.error_dict.get('pullrequest_title')
1117 msg = errors.error_dict.get('pullrequest_title')
1118 else:
1118 else:
1119 msg = _('Error creating pull request: {}').format(errors)
1119 msg = _('Error creating pull request: {}').format(errors)
1120 log.exception(msg)
1120 log.exception(msg)
1121 h.flash(msg, 'error')
1121 h.flash(msg, 'error')
1122
1122
1123 # would rather just go back to form ...
1123 # would rather just go back to form ...
1124 raise HTTPFound(
1124 raise HTTPFound(
1125 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1125 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1126
1126
1127 source_repo = _form['source_repo']
1127 source_repo = _form['source_repo']
1128 source_ref = _form['source_ref']
1128 source_ref = _form['source_ref']
1129 target_repo = _form['target_repo']
1129 target_repo = _form['target_repo']
1130 target_ref = _form['target_ref']
1130 target_ref = _form['target_ref']
1131 commit_ids = _form['revisions'][::-1]
1131 commit_ids = _form['revisions'][::-1]
1132 common_ancestor_id = _form['common_ancestor']
1132 common_ancestor_id = _form['common_ancestor']
1133
1133
1134 # find the ancestor for this pr
1134 # find the ancestor for this pr
1135 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1135 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1136 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1136 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1137
1137
1138 if not (source_db_repo or target_db_repo):
1138 if not (source_db_repo or target_db_repo):
1139 h.flash(_('source_repo or target repo not found'), category='error')
1139 h.flash(_('source_repo or target repo not found'), category='error')
1140 raise HTTPFound(
1140 raise HTTPFound(
1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1142
1142
1143 # re-check permissions again here
1143 # re-check permissions again here
1144 # source_repo we must have read permissions
1144 # source_repo we must have read permissions
1145
1145
1146 source_perm = HasRepoPermissionAny(
1146 source_perm = HasRepoPermissionAny(
1147 'repository.read', 'repository.write', 'repository.admin')(
1147 'repository.read', 'repository.write', 'repository.admin')(
1148 source_db_repo.repo_name)
1148 source_db_repo.repo_name)
1149 if not source_perm:
1149 if not source_perm:
1150 msg = _('Not Enough permissions to source repo `{}`.'.format(
1150 msg = _('Not Enough permissions to source repo `{}`.'.format(
1151 source_db_repo.repo_name))
1151 source_db_repo.repo_name))
1152 h.flash(msg, category='error')
1152 h.flash(msg, category='error')
1153 # copy the args back to redirect
1153 # copy the args back to redirect
1154 org_query = self.request.GET.mixed()
1154 org_query = self.request.GET.mixed()
1155 raise HTTPFound(
1155 raise HTTPFound(
1156 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1156 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1157 _query=org_query))
1157 _query=org_query))
1158
1158
1159 # target repo we must have read permissions, and also later on
1159 # target repo we must have read permissions, and also later on
1160 # we want to check branch permissions here
1160 # we want to check branch permissions here
1161 target_perm = HasRepoPermissionAny(
1161 target_perm = HasRepoPermissionAny(
1162 'repository.read', 'repository.write', 'repository.admin')(
1162 'repository.read', 'repository.write', 'repository.admin')(
1163 target_db_repo.repo_name)
1163 target_db_repo.repo_name)
1164 if not target_perm:
1164 if not target_perm:
1165 msg = _('Not Enough permissions to target repo `{}`.'.format(
1165 msg = _('Not Enough permissions to target repo `{}`.'.format(
1166 target_db_repo.repo_name))
1166 target_db_repo.repo_name))
1167 h.flash(msg, category='error')
1167 h.flash(msg, category='error')
1168 # copy the args back to redirect
1168 # copy the args back to redirect
1169 org_query = self.request.GET.mixed()
1169 org_query = self.request.GET.mixed()
1170 raise HTTPFound(
1170 raise HTTPFound(
1171 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1171 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1172 _query=org_query))
1172 _query=org_query))
1173
1173
1174 source_scm = source_db_repo.scm_instance()
1174 source_scm = source_db_repo.scm_instance()
1175 target_scm = target_db_repo.scm_instance()
1175 target_scm = target_db_repo.scm_instance()
1176
1176
1177 source_ref_obj = unicode_to_reference(source_ref)
1177 source_ref_obj = unicode_to_reference(source_ref)
1178 target_ref_obj = unicode_to_reference(target_ref)
1178 target_ref_obj = unicode_to_reference(target_ref)
1179
1179
1180 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1180 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1181 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1181 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1182
1182
1183 ancestor = source_scm.get_common_ancestor(
1183 ancestor = source_scm.get_common_ancestor(
1184 source_commit.raw_id, target_commit.raw_id, target_scm)
1184 source_commit.raw_id, target_commit.raw_id, target_scm)
1185
1185
1186 # recalculate target ref based on ancestor
1186 # recalculate target ref based on ancestor
1187 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1187 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1188
1188
1189 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1189 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1190 PullRequestModel().get_reviewer_functions()
1190 PullRequestModel().get_reviewer_functions()
1191
1191
1192 # recalculate reviewers logic, to make sure we can validate this
1192 # recalculate reviewers logic, to make sure we can validate this
1193 reviewer_rules = get_default_reviewers_data(
1193 reviewer_rules = get_default_reviewers_data(
1194 self._rhodecode_db_user,
1194 self._rhodecode_db_user,
1195 source_db_repo,
1195 source_db_repo,
1196 source_ref_obj,
1196 source_ref_obj,
1197 target_db_repo,
1197 target_db_repo,
1198 target_ref_obj,
1198 target_ref_obj,
1199 include_diff_info=False)
1199 include_diff_info=False)
1200
1200
1201 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1201 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1202 observers = validate_observers(_form['observer_members'], reviewer_rules)
1202 observers = validate_observers(_form['observer_members'], reviewer_rules)
1203
1203
1204 pullrequest_title = _form['pullrequest_title']
1204 pullrequest_title = _form['pullrequest_title']
1205 title_source_ref = source_ref_obj.name
1205 title_source_ref = source_ref_obj.name
1206 if not pullrequest_title:
1206 if not pullrequest_title:
1207 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1207 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1208 source=source_repo,
1208 source=source_repo,
1209 source_ref=title_source_ref,
1209 source_ref=title_source_ref,
1210 target=target_repo
1210 target=target_repo
1211 )
1211 )
1212
1212
1213 description = _form['pullrequest_desc']
1213 description = _form['pullrequest_desc']
1214 description_renderer = _form['description_renderer']
1214 description_renderer = _form['description_renderer']
1215
1215
1216 try:
1216 try:
1217 pull_request = PullRequestModel().create(
1217 pull_request = PullRequestModel().create(
1218 created_by=self._rhodecode_user.user_id,
1218 created_by=self._rhodecode_user.user_id,
1219 source_repo=source_repo,
1219 source_repo=source_repo,
1220 source_ref=source_ref,
1220 source_ref=source_ref,
1221 target_repo=target_repo,
1221 target_repo=target_repo,
1222 target_ref=target_ref,
1222 target_ref=target_ref,
1223 revisions=commit_ids,
1223 revisions=commit_ids,
1224 common_ancestor_id=common_ancestor_id,
1224 common_ancestor_id=common_ancestor_id,
1225 reviewers=reviewers,
1225 reviewers=reviewers,
1226 observers=observers,
1226 observers=observers,
1227 title=pullrequest_title,
1227 title=pullrequest_title,
1228 description=description,
1228 description=description,
1229 description_renderer=description_renderer,
1229 description_renderer=description_renderer,
1230 reviewer_data=reviewer_rules,
1230 reviewer_data=reviewer_rules,
1231 auth_user=self._rhodecode_user
1231 auth_user=self._rhodecode_user
1232 )
1232 )
1233 Session().commit()
1233 Session().commit()
1234
1234
1235 h.flash(_('Successfully opened new pull request'),
1235 h.flash(_('Successfully opened new pull request'),
1236 category='success')
1236 category='success')
1237 except Exception:
1237 except Exception:
1238 msg = _('Error occurred during creation of this pull request.')
1238 msg = _('Error occurred during creation of this pull request.')
1239 log.exception(msg)
1239 log.exception(msg)
1240 h.flash(msg, category='error')
1240 h.flash(msg, category='error')
1241
1241
1242 # copy the args back to redirect
1242 # copy the args back to redirect
1243 org_query = self.request.GET.mixed()
1243 org_query = self.request.GET.mixed()
1244 raise HTTPFound(
1244 raise HTTPFound(
1245 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1245 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1246 _query=org_query))
1246 _query=org_query))
1247
1247
1248 raise HTTPFound(
1248 raise HTTPFound(
1249 h.route_path('pullrequest_show', repo_name=target_repo,
1249 h.route_path('pullrequest_show', repo_name=target_repo,
1250 pull_request_id=pull_request.pull_request_id))
1250 pull_request_id=pull_request.pull_request_id))
1251
1251
1252 @LoginRequired()
1252 @LoginRequired()
1253 @NotAnonymous()
1253 @NotAnonymous()
1254 @HasRepoPermissionAnyDecorator(
1254 @HasRepoPermissionAnyDecorator(
1255 'repository.read', 'repository.write', 'repository.admin')
1255 'repository.read', 'repository.write', 'repository.admin')
1256 @CSRFRequired()
1256 @CSRFRequired()
1257 def pull_request_update(self):
1257 def pull_request_update(self):
1258 pull_request = PullRequest.get_or_404(
1258 pull_request = PullRequest.get_or_404(
1259 self.request.matchdict['pull_request_id'])
1259 self.request.matchdict['pull_request_id'])
1260 _ = self.request.translate
1260 _ = self.request.translate
1261
1261
1262 c = self.load_default_context()
1262 c = self.load_default_context()
1263 redirect_url = None
1263 redirect_url = None
1264
1264
1265 if pull_request.is_closed():
1265 if pull_request.is_closed():
1266 log.debug('update: forbidden because pull request is closed')
1266 log.debug('update: forbidden because pull request is closed')
1267 msg = _(u'Cannot update closed pull requests.')
1267 msg = _(u'Cannot update closed pull requests.')
1268 h.flash(msg, category='error')
1268 h.flash(msg, category='error')
1269 return {'response': True,
1269 return {'response': True,
1270 'redirect_url': redirect_url}
1270 'redirect_url': redirect_url}
1271
1271
1272 is_state_changing = pull_request.is_state_changing()
1272 is_state_changing = pull_request.is_state_changing()
1273 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1273 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1274
1274
1275 # only owner or admin can update it
1275 # only owner or admin can update it
1276 allowed_to_update = PullRequestModel().check_user_update(
1276 allowed_to_update = PullRequestModel().check_user_update(
1277 pull_request, self._rhodecode_user)
1277 pull_request, self._rhodecode_user)
1278
1278
1279 if allowed_to_update:
1279 if allowed_to_update:
1280 controls = peppercorn.parse(self.request.POST.items())
1280 controls = peppercorn.parse(self.request.POST.items())
1281 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1281 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1282
1282
1283 if 'review_members' in controls:
1283 if 'review_members' in controls:
1284 self._update_reviewers(
1284 self._update_reviewers(
1285 c,
1285 c,
1286 pull_request, controls['review_members'],
1286 pull_request, controls['review_members'],
1287 pull_request.reviewer_data,
1287 pull_request.reviewer_data,
1288 PullRequestReviewers.ROLE_REVIEWER)
1288 PullRequestReviewers.ROLE_REVIEWER)
1289 elif 'observer_members' in controls:
1289 elif 'observer_members' in controls:
1290 self._update_reviewers(
1290 self._update_reviewers(
1291 c,
1291 c,
1292 pull_request, controls['observer_members'],
1292 pull_request, controls['observer_members'],
1293 pull_request.reviewer_data,
1293 pull_request.reviewer_data,
1294 PullRequestReviewers.ROLE_OBSERVER)
1294 PullRequestReviewers.ROLE_OBSERVER)
1295 elif str2bool(self.request.POST.get('update_commits', 'false')):
1295 elif str2bool(self.request.POST.get('update_commits', 'false')):
1296 if is_state_changing:
1296 if is_state_changing:
1297 log.debug('commits update: forbidden because pull request is in state %s',
1297 log.debug('commits update: forbidden because pull request is in state %s',
1298 pull_request.pull_request_state)
1298 pull_request.pull_request_state)
1299 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1299 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1300 u'Current state is: `{}`').format(
1300 u'Current state is: `{}`').format(
1301 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1301 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1302 h.flash(msg, category='error')
1302 h.flash(msg, category='error')
1303 return {'response': True,
1303 return {'response': True,
1304 'redirect_url': redirect_url}
1304 'redirect_url': redirect_url}
1305
1305
1306 self._update_commits(c, pull_request)
1306 self._update_commits(c, pull_request)
1307 if force_refresh:
1307 if force_refresh:
1308 redirect_url = h.route_path(
1308 redirect_url = h.route_path(
1309 'pullrequest_show', repo_name=self.db_repo_name,
1309 'pullrequest_show', repo_name=self.db_repo_name,
1310 pull_request_id=pull_request.pull_request_id,
1310 pull_request_id=pull_request.pull_request_id,
1311 _query={"force_refresh": 1})
1311 _query={"force_refresh": 1})
1312 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1312 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1313 self._edit_pull_request(pull_request)
1313 self._edit_pull_request(pull_request)
1314 else:
1314 else:
1315 log.error('Unhandled update data.')
1315 log.error('Unhandled update data.')
1316 raise HTTPBadRequest()
1316 raise HTTPBadRequest()
1317
1317
1318 return {'response': True,
1318 return {'response': True,
1319 'redirect_url': redirect_url}
1319 'redirect_url': redirect_url}
1320 raise HTTPForbidden()
1320 raise HTTPForbidden()
1321
1321
1322 def _edit_pull_request(self, pull_request):
1322 def _edit_pull_request(self, pull_request):
1323 """
1323 """
1324 Edit title and description
1324 Edit title and description
1325 """
1325 """
1326 _ = self.request.translate
1326 _ = self.request.translate
1327
1327
1328 try:
1328 try:
1329 PullRequestModel().edit(
1329 PullRequestModel().edit(
1330 pull_request,
1330 pull_request,
1331 self.request.POST.get('title'),
1331 self.request.POST.get('title'),
1332 self.request.POST.get('description'),
1332 self.request.POST.get('description'),
1333 self.request.POST.get('description_renderer'),
1333 self.request.POST.get('description_renderer'),
1334 self._rhodecode_user)
1334 self._rhodecode_user)
1335 except ValueError:
1335 except ValueError:
1336 msg = _(u'Cannot update closed pull requests.')
1336 msg = _(u'Cannot update closed pull requests.')
1337 h.flash(msg, category='error')
1337 h.flash(msg, category='error')
1338 return
1338 return
1339 else:
1339 else:
1340 Session().commit()
1340 Session().commit()
1341
1341
1342 msg = _(u'Pull request title & description updated.')
1342 msg = _(u'Pull request title & description updated.')
1343 h.flash(msg, category='success')
1343 h.flash(msg, category='success')
1344 return
1344 return
1345
1345
1346 def _update_commits(self, c, pull_request):
1346 def _update_commits(self, c, pull_request):
1347 _ = self.request.translate
1347 _ = self.request.translate
1348
1348
1349 with pull_request.set_state(PullRequest.STATE_UPDATING):
1349 with pull_request.set_state(PullRequest.STATE_UPDATING):
1350 resp = PullRequestModel().update_commits(
1350 resp = PullRequestModel().update_commits(
1351 pull_request, self._rhodecode_db_user)
1351 pull_request, self._rhodecode_db_user)
1352
1352
1353 if resp.executed:
1353 if resp.executed:
1354
1354
1355 if resp.target_changed and resp.source_changed:
1355 if resp.target_changed and resp.source_changed:
1356 changed = 'target and source repositories'
1356 changed = 'target and source repositories'
1357 elif resp.target_changed and not resp.source_changed:
1357 elif resp.target_changed and not resp.source_changed:
1358 changed = 'target repository'
1358 changed = 'target repository'
1359 elif not resp.target_changed and resp.source_changed:
1359 elif not resp.target_changed and resp.source_changed:
1360 changed = 'source repository'
1360 changed = 'source repository'
1361 else:
1361 else:
1362 changed = 'nothing'
1362 changed = 'nothing'
1363
1363
1364 msg = _(u'Pull request updated to "{source_commit_id}" with '
1364 msg = _(u'Pull request updated to "{source_commit_id}" with '
1365 u'{count_added} added, {count_removed} removed commits. '
1365 u'{count_added} added, {count_removed} removed commits. '
1366 u'Source of changes: {change_source}.')
1366 u'Source of changes: {change_source}.')
1367 msg = msg.format(
1367 msg = msg.format(
1368 source_commit_id=pull_request.source_ref_parts.commit_id,
1368 source_commit_id=pull_request.source_ref_parts.commit_id,
1369 count_added=len(resp.changes.added),
1369 count_added=len(resp.changes.added),
1370 count_removed=len(resp.changes.removed),
1370 count_removed=len(resp.changes.removed),
1371 change_source=changed)
1371 change_source=changed)
1372 h.flash(msg, category='success')
1372 h.flash(msg, category='success')
1373 channelstream.pr_update_channelstream_push(
1373 channelstream.pr_update_channelstream_push(
1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1375 else:
1375 else:
1376 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1376 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1377 warning_reasons = [
1377 warning_reasons = [
1378 UpdateFailureReason.NO_CHANGE,
1378 UpdateFailureReason.NO_CHANGE,
1379 UpdateFailureReason.WRONG_REF_TYPE,
1379 UpdateFailureReason.WRONG_REF_TYPE,
1380 ]
1380 ]
1381 category = 'warning' if resp.reason in warning_reasons else 'error'
1381 category = 'warning' if resp.reason in warning_reasons else 'error'
1382 h.flash(msg, category=category)
1382 h.flash(msg, category=category)
1383
1383
1384 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1384 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1385 _ = self.request.translate
1385 _ = self.request.translate
1386
1386
1387 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1387 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1388 PullRequestModel().get_reviewer_functions()
1388 PullRequestModel().get_reviewer_functions()
1389
1389
1390 if role == PullRequestReviewers.ROLE_REVIEWER:
1390 if role == PullRequestReviewers.ROLE_REVIEWER:
1391 try:
1391 try:
1392 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1392 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1393 except ValueError as e:
1393 except ValueError as e:
1394 log.error('Reviewers Validation: {}'.format(e))
1394 log.error('Reviewers Validation: {}'.format(e))
1395 h.flash(e, category='error')
1395 h.flash(e, category='error')
1396 return
1396 return
1397
1397
1398 old_calculated_status = pull_request.calculated_review_status()
1398 old_calculated_status = pull_request.calculated_review_status()
1399 PullRequestModel().update_reviewers(
1399 PullRequestModel().update_reviewers(
1400 pull_request, reviewers, self._rhodecode_db_user)
1400 pull_request, reviewers, self._rhodecode_db_user)
1401
1401
1402 Session().commit()
1402 Session().commit()
1403
1403
1404 msg = _('Pull request reviewers updated.')
1404 msg = _('Pull request reviewers updated.')
1405 h.flash(msg, category='success')
1405 h.flash(msg, category='success')
1406 channelstream.pr_update_channelstream_push(
1406 channelstream.pr_update_channelstream_push(
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1408
1408
1409 # trigger status changed if change in reviewers changes the status
1409 # trigger status changed if change in reviewers changes the status
1410 calculated_status = pull_request.calculated_review_status()
1410 calculated_status = pull_request.calculated_review_status()
1411 if old_calculated_status != calculated_status:
1411 if old_calculated_status != calculated_status:
1412 PullRequestModel().trigger_pull_request_hook(
1412 PullRequestModel().trigger_pull_request_hook(
1413 pull_request, self._rhodecode_user, 'review_status_change',
1413 pull_request, self._rhodecode_user, 'review_status_change',
1414 data={'status': calculated_status})
1414 data={'status': calculated_status})
1415
1415
1416 elif role == PullRequestReviewers.ROLE_OBSERVER:
1416 elif role == PullRequestReviewers.ROLE_OBSERVER:
1417 try:
1417 try:
1418 observers = validate_observers(review_members, reviewer_rules)
1418 observers = validate_observers(review_members, reviewer_rules)
1419 except ValueError as e:
1419 except ValueError as e:
1420 log.error('Observers Validation: {}'.format(e))
1420 log.error('Observers Validation: {}'.format(e))
1421 h.flash(e, category='error')
1421 h.flash(e, category='error')
1422 return
1422 return
1423
1423
1424 PullRequestModel().update_observers(
1424 PullRequestModel().update_observers(
1425 pull_request, observers, self._rhodecode_db_user)
1425 pull_request, observers, self._rhodecode_db_user)
1426
1426
1427 Session().commit()
1427 Session().commit()
1428 msg = _('Pull request observers updated.')
1428 msg = _('Pull request observers updated.')
1429 h.flash(msg, category='success')
1429 h.flash(msg, category='success')
1430 channelstream.pr_update_channelstream_push(
1430 channelstream.pr_update_channelstream_push(
1431 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1431 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1432
1432
1433 @LoginRequired()
1433 @LoginRequired()
1434 @NotAnonymous()
1434 @NotAnonymous()
1435 @HasRepoPermissionAnyDecorator(
1435 @HasRepoPermissionAnyDecorator(
1436 'repository.read', 'repository.write', 'repository.admin')
1436 'repository.read', 'repository.write', 'repository.admin')
1437 @CSRFRequired()
1437 @CSRFRequired()
1438 def pull_request_merge(self):
1438 def pull_request_merge(self):
1439 """
1439 """
1440 Merge will perform a server-side merge of the specified
1440 Merge will perform a server-side merge of the specified
1441 pull request, if the pull request is approved and mergeable.
1441 pull request, if the pull request is approved and mergeable.
1442 After successful merging, the pull request is automatically
1442 After successful merging, the pull request is automatically
1443 closed, with a relevant comment.
1443 closed, with a relevant comment.
1444 """
1444 """
1445 pull_request = PullRequest.get_or_404(
1445 pull_request = PullRequest.get_or_404(
1446 self.request.matchdict['pull_request_id'])
1446 self.request.matchdict['pull_request_id'])
1447 _ = self.request.translate
1447 _ = self.request.translate
1448
1448
1449 if pull_request.is_state_changing():
1449 if pull_request.is_state_changing():
1450 log.debug('show: forbidden because pull request is in state %s',
1450 log.debug('show: forbidden because pull request is in state %s',
1451 pull_request.pull_request_state)
1451 pull_request.pull_request_state)
1452 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1452 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1453 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1453 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1454 pull_request.pull_request_state)
1454 pull_request.pull_request_state)
1455 h.flash(msg, category='error')
1455 h.flash(msg, category='error')
1456 raise HTTPFound(
1456 raise HTTPFound(
1457 h.route_path('pullrequest_show',
1457 h.route_path('pullrequest_show',
1458 repo_name=pull_request.target_repo.repo_name,
1458 repo_name=pull_request.target_repo.repo_name,
1459 pull_request_id=pull_request.pull_request_id))
1459 pull_request_id=pull_request.pull_request_id))
1460
1460
1461 self.load_default_context()
1461 self.load_default_context()
1462
1462
1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1464 check = MergeCheck.validate(
1464 check = MergeCheck.validate(
1465 pull_request, auth_user=self._rhodecode_user,
1465 pull_request, auth_user=self._rhodecode_user,
1466 translator=self.request.translate)
1466 translator=self.request.translate)
1467 merge_possible = not check.failed
1467 merge_possible = not check.failed
1468
1468
1469 for err_type, error_msg in check.errors:
1469 for err_type, error_msg in check.errors:
1470 h.flash(error_msg, category=err_type)
1470 h.flash(error_msg, category=err_type)
1471
1471
1472 if merge_possible:
1472 if merge_possible:
1473 log.debug("Pre-conditions checked, trying to merge.")
1473 log.debug("Pre-conditions checked, trying to merge.")
1474 extras = vcs_operation_context(
1474 extras = vcs_operation_context(
1475 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1475 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1476 username=self._rhodecode_db_user.username, action='push',
1476 username=self._rhodecode_db_user.username, action='push',
1477 scm=pull_request.target_repo.repo_type)
1477 scm=pull_request.target_repo.repo_type)
1478 with pull_request.set_state(PullRequest.STATE_UPDATING):
1478 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 self._merge_pull_request(
1479 self._merge_pull_request(
1480 pull_request, self._rhodecode_db_user, extras)
1480 pull_request, self._rhodecode_db_user, extras)
1481 else:
1481 else:
1482 log.debug("Pre-conditions failed, NOT merging.")
1482 log.debug("Pre-conditions failed, NOT merging.")
1483
1483
1484 raise HTTPFound(
1484 raise HTTPFound(
1485 h.route_path('pullrequest_show',
1485 h.route_path('pullrequest_show',
1486 repo_name=pull_request.target_repo.repo_name,
1486 repo_name=pull_request.target_repo.repo_name,
1487 pull_request_id=pull_request.pull_request_id))
1487 pull_request_id=pull_request.pull_request_id))
1488
1488
1489 def _merge_pull_request(self, pull_request, user, extras):
1489 def _merge_pull_request(self, pull_request, user, extras):
1490 _ = self.request.translate
1490 _ = self.request.translate
1491 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1491 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1492
1492
1493 if merge_resp.executed:
1493 if merge_resp.executed:
1494 log.debug("The merge was successful, closing the pull request.")
1494 log.debug("The merge was successful, closing the pull request.")
1495 PullRequestModel().close_pull_request(
1495 PullRequestModel().close_pull_request(
1496 pull_request.pull_request_id, user)
1496 pull_request.pull_request_id, user)
1497 Session().commit()
1497 Session().commit()
1498 msg = _('Pull request was successfully merged and closed.')
1498 msg = _('Pull request was successfully merged and closed.')
1499 h.flash(msg, category='success')
1499 h.flash(msg, category='success')
1500 else:
1500 else:
1501 log.debug(
1501 log.debug(
1502 "The merge was not successful. Merge response: %s", merge_resp)
1502 "The merge was not successful. Merge response: %s", merge_resp)
1503 msg = merge_resp.merge_status_message
1503 msg = merge_resp.merge_status_message
1504 h.flash(msg, category='error')
1504 h.flash(msg, category='error')
1505
1505
1506 @LoginRequired()
1506 @LoginRequired()
1507 @NotAnonymous()
1507 @NotAnonymous()
1508 @HasRepoPermissionAnyDecorator(
1508 @HasRepoPermissionAnyDecorator(
1509 'repository.read', 'repository.write', 'repository.admin')
1509 'repository.read', 'repository.write', 'repository.admin')
1510 @CSRFRequired()
1510 @CSRFRequired()
1511 def pull_request_delete(self):
1511 def pull_request_delete(self):
1512 _ = self.request.translate
1512 _ = self.request.translate
1513
1513
1514 pull_request = PullRequest.get_or_404(
1514 pull_request = PullRequest.get_or_404(
1515 self.request.matchdict['pull_request_id'])
1515 self.request.matchdict['pull_request_id'])
1516 self.load_default_context()
1516 self.load_default_context()
1517
1517
1518 pr_closed = pull_request.is_closed()
1518 pr_closed = pull_request.is_closed()
1519 allowed_to_delete = PullRequestModel().check_user_delete(
1519 allowed_to_delete = PullRequestModel().check_user_delete(
1520 pull_request, self._rhodecode_user) and not pr_closed
1520 pull_request, self._rhodecode_user) and not pr_closed
1521
1521
1522 # only owner can delete it !
1522 # only owner can delete it !
1523 if allowed_to_delete:
1523 if allowed_to_delete:
1524 PullRequestModel().delete(pull_request, self._rhodecode_user)
1524 PullRequestModel().delete(pull_request, self._rhodecode_user)
1525 Session().commit()
1525 Session().commit()
1526 h.flash(_('Successfully deleted pull request'),
1526 h.flash(_('Successfully deleted pull request'),
1527 category='success')
1527 category='success')
1528 raise HTTPFound(h.route_path('pullrequest_show_all',
1528 raise HTTPFound(h.route_path('pullrequest_show_all',
1529 repo_name=self.db_repo_name))
1529 repo_name=self.db_repo_name))
1530
1530
1531 log.warning('user %s tried to delete pull request without access',
1531 log.warning('user %s tried to delete pull request without access',
1532 self._rhodecode_user)
1532 self._rhodecode_user)
1533 raise HTTPNotFound()
1533 raise HTTPNotFound()
1534
1534
1535 def _pull_request_comments_create(self, pull_request, comments):
1535 def _pull_request_comments_create(self, pull_request, comments):
1536 _ = self.request.translate
1536 _ = self.request.translate
1537 data = {}
1537 data = {}
1538 if not comments:
1538 if not comments:
1539 return
1539 return
1540 pull_request_id = pull_request.pull_request_id
1540 pull_request_id = pull_request.pull_request_id
1541
1541
1542 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1542 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1543
1543
1544 for entry in comments:
1544 for entry in comments:
1545 c = self.load_default_context()
1545 c = self.load_default_context()
1546 comment_type = entry['comment_type']
1546 comment_type = entry['comment_type']
1547 text = entry['text']
1547 text = entry['text']
1548 status = entry['status']
1548 status = entry['status']
1549 is_draft = str2bool(entry['is_draft'])
1549 is_draft = str2bool(entry['is_draft'])
1550 resolves_comment_id = entry['resolves_comment_id']
1550 resolves_comment_id = entry['resolves_comment_id']
1551 close_pull_request = entry['close_pull_request']
1551 close_pull_request = entry['close_pull_request']
1552 f_path = entry['f_path']
1552 f_path = entry['f_path']
1553 line_no = entry['line']
1553 line_no = entry['line']
1554 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1554 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1555
1555
1556 # the logic here should work like following, if we submit close
1556 # the logic here should work like following, if we submit close
1557 # pr comment, use `close_pull_request_with_comment` function
1557 # pr comment, use `close_pull_request_with_comment` function
1558 # else handle regular comment logic
1558 # else handle regular comment logic
1559
1559
1560 if close_pull_request:
1560 if close_pull_request:
1561 # only owner or admin or person with write permissions
1561 # only owner or admin or person with write permissions
1562 allowed_to_close = PullRequestModel().check_user_update(
1562 allowed_to_close = PullRequestModel().check_user_update(
1563 pull_request, self._rhodecode_user)
1563 pull_request, self._rhodecode_user)
1564 if not allowed_to_close:
1564 if not allowed_to_close:
1565 log.debug('comment: forbidden because not allowed to close '
1565 log.debug('comment: forbidden because not allowed to close '
1566 'pull request %s', pull_request_id)
1566 'pull request %s', pull_request_id)
1567 raise HTTPForbidden()
1567 raise HTTPForbidden()
1568
1568
1569 # This also triggers `review_status_change`
1569 # This also triggers `review_status_change`
1570 comment, status = PullRequestModel().close_pull_request_with_comment(
1570 comment, status = PullRequestModel().close_pull_request_with_comment(
1571 pull_request, self._rhodecode_user, self.db_repo, message=text,
1571 pull_request, self._rhodecode_user, self.db_repo, message=text,
1572 auth_user=self._rhodecode_user)
1572 auth_user=self._rhodecode_user)
1573 Session().flush()
1573 Session().flush()
1574 is_inline = comment.is_inline
1574 is_inline = comment.is_inline
1575
1575
1576 PullRequestModel().trigger_pull_request_hook(
1576 PullRequestModel().trigger_pull_request_hook(
1577 pull_request, self._rhodecode_user, 'comment',
1577 pull_request, self._rhodecode_user, 'comment',
1578 data={'comment': comment})
1578 data={'comment': comment})
1579
1579
1580 else:
1580 else:
1581 # regular comment case, could be inline, or one with status.
1581 # regular comment case, could be inline, or one with status.
1582 # for that one we check also permissions
1582 # for that one we check also permissions
1583 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1583 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1584 allowed_to_change_status = PullRequestModel().check_user_change_status(
1584 allowed_to_change_status = PullRequestModel().check_user_change_status(
1585 pull_request, self._rhodecode_user) and not is_draft
1585 pull_request, self._rhodecode_user) and not is_draft
1586
1586
1587 if status and allowed_to_change_status:
1587 if status and allowed_to_change_status:
1588 message = (_('Status change %(transition_icon)s %(status)s')
1588 message = (_('Status change %(transition_icon)s %(status)s')
1589 % {'transition_icon': '>',
1589 % {'transition_icon': '>',
1590 'status': ChangesetStatus.get_status_lbl(status)})
1590 'status': ChangesetStatus.get_status_lbl(status)})
1591 text = text or message
1591 text = text or message
1592
1592
1593 comment = CommentsModel().create(
1593 comment = CommentsModel().create(
1594 text=text,
1594 text=text,
1595 repo=self.db_repo.repo_id,
1595 repo=self.db_repo.repo_id,
1596 user=self._rhodecode_user.user_id,
1596 user=self._rhodecode_user.user_id,
1597 pull_request=pull_request,
1597 pull_request=pull_request,
1598 f_path=f_path,
1598 f_path=f_path,
1599 line_no=line_no,
1599 line_no=line_no,
1600 status_change=(ChangesetStatus.get_status_lbl(status)
1600 status_change=(ChangesetStatus.get_status_lbl(status)
1601 if status and allowed_to_change_status else None),
1601 if status and allowed_to_change_status else None),
1602 status_change_type=(status
1602 status_change_type=(status
1603 if status and allowed_to_change_status else None),
1603 if status and allowed_to_change_status else None),
1604 comment_type=comment_type,
1604 comment_type=comment_type,
1605 is_draft=is_draft,
1605 is_draft=is_draft,
1606 resolves_comment_id=resolves_comment_id,
1606 resolves_comment_id=resolves_comment_id,
1607 auth_user=self._rhodecode_user,
1607 auth_user=self._rhodecode_user,
1608 send_email=not is_draft, # skip notification for draft comments
1608 send_email=not is_draft, # skip notification for draft comments
1609 )
1609 )
1610 is_inline = comment.is_inline
1610 is_inline = comment.is_inline
1611
1611
1612 if allowed_to_change_status:
1612 if allowed_to_change_status:
1613 # calculate old status before we change it
1613 # calculate old status before we change it
1614 old_calculated_status = pull_request.calculated_review_status()
1614 old_calculated_status = pull_request.calculated_review_status()
1615
1615
1616 # get status if set !
1616 # get status if set !
1617 if status:
1617 if status:
1618 ChangesetStatusModel().set_status(
1618 ChangesetStatusModel().set_status(
1619 self.db_repo.repo_id,
1619 self.db_repo.repo_id,
1620 status,
1620 status,
1621 self._rhodecode_user.user_id,
1621 self._rhodecode_user.user_id,
1622 comment,
1622 comment,
1623 pull_request=pull_request
1623 pull_request=pull_request
1624 )
1624 )
1625
1625
1626 Session().flush()
1626 Session().flush()
1627 # this is somehow required to get access to some relationship
1627 # this is somehow required to get access to some relationship
1628 # loaded on comment
1628 # loaded on comment
1629 Session().refresh(comment)
1629 Session().refresh(comment)
1630
1630
1631 # skip notifications for drafts
1631 # skip notifications for drafts
1632 if not is_draft:
1632 if not is_draft:
1633 PullRequestModel().trigger_pull_request_hook(
1633 PullRequestModel().trigger_pull_request_hook(
1634 pull_request, self._rhodecode_user, 'comment',
1634 pull_request, self._rhodecode_user, 'comment',
1635 data={'comment': comment})
1635 data={'comment': comment})
1636
1636
1637 # we now calculate the status of pull request, and based on that
1637 # we now calculate the status of pull request, and based on that
1638 # calculation we set the commits status
1638 # calculation we set the commits status
1639 calculated_status = pull_request.calculated_review_status()
1639 calculated_status = pull_request.calculated_review_status()
1640 if old_calculated_status != calculated_status:
1640 if old_calculated_status != calculated_status:
1641 PullRequestModel().trigger_pull_request_hook(
1641 PullRequestModel().trigger_pull_request_hook(
1642 pull_request, self._rhodecode_user, 'review_status_change',
1642 pull_request, self._rhodecode_user, 'review_status_change',
1643 data={'status': calculated_status})
1643 data={'status': calculated_status})
1644
1644
1645 comment_id = comment.comment_id
1645 comment_id = comment.comment_id
1646 data[comment_id] = {
1646 data[comment_id] = {
1647 'target_id': target_elem_id
1647 'target_id': target_elem_id
1648 }
1648 }
1649 Session().flush()
1649 Session().flush()
1650
1650
1651 c.co = comment
1651 c.co = comment
1652 c.at_version_num = None
1652 c.at_version_num = None
1653 c.is_new = True
1653 c.is_new = True
1654 rendered_comment = render(
1654 rendered_comment = render(
1655 'rhodecode:templates/changeset/changeset_comment_block.mako',
1655 'rhodecode:templates/changeset/changeset_comment_block.mako',
1656 self._get_template_context(c), self.request)
1656 self._get_template_context(c), self.request)
1657
1657
1658 data[comment_id].update(comment.get_dict())
1658 data[comment_id].update(comment.get_dict())
1659 data[comment_id].update({'rendered_text': rendered_comment})
1659 data[comment_id].update({'rendered_text': rendered_comment})
1660
1660
1661 Session().commit()
1661 Session().commit()
1662
1662
1663 # skip channelstream for draft comments
1663 # skip channelstream for draft comments
1664 if not all_drafts:
1664 if not all_drafts:
1665 comment_broadcast_channel = channelstream.comment_channel(
1665 comment_broadcast_channel = channelstream.comment_channel(
1666 self.db_repo_name, pull_request_obj=pull_request)
1666 self.db_repo_name, pull_request_obj=pull_request)
1667
1667
1668 comment_data = data
1668 comment_data = data
1669 posted_comment_type = 'inline' if is_inline else 'general'
1669 posted_comment_type = 'inline' if is_inline else 'general'
1670 if len(data) == 1:
1670 if len(data) == 1:
1671 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1671 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1672 else:
1672 else:
1673 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1673 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1674
1674
1675 channelstream.comment_channelstream_push(
1675 channelstream.comment_channelstream_push(
1676 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1676 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1677 comment_data=comment_data)
1677 comment_data=comment_data)
1678
1678
1679 return data
1679 return data
1680
1680
1681 @LoginRequired()
1681 @LoginRequired()
1682 @NotAnonymous()
1682 @NotAnonymous()
1683 @HasRepoPermissionAnyDecorator(
1683 @HasRepoPermissionAnyDecorator(
1684 'repository.read', 'repository.write', 'repository.admin')
1684 'repository.read', 'repository.write', 'repository.admin')
1685 @CSRFRequired()
1685 @CSRFRequired()
1686 def pull_request_comment_create(self):
1686 def pull_request_comment_create(self):
1687 _ = self.request.translate
1687 _ = self.request.translate
1688
1688
1689 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1689 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1690
1690
1691 if pull_request.is_closed():
1691 if pull_request.is_closed():
1692 log.debug('comment: forbidden because pull request is closed')
1692 log.debug('comment: forbidden because pull request is closed')
1693 raise HTTPForbidden()
1693 raise HTTPForbidden()
1694
1694
1695 allowed_to_comment = PullRequestModel().check_user_comment(
1695 allowed_to_comment = PullRequestModel().check_user_comment(
1696 pull_request, self._rhodecode_user)
1696 pull_request, self._rhodecode_user)
1697 if not allowed_to_comment:
1697 if not allowed_to_comment:
1698 log.debug('comment: forbidden because pull request is from forbidden repo')
1698 log.debug('comment: forbidden because pull request is from forbidden repo')
1699 raise HTTPForbidden()
1699 raise HTTPForbidden()
1700
1700
1701 comment_data = {
1701 comment_data = {
1702 'comment_type': self.request.POST.get('comment_type'),
1702 'comment_type': self.request.POST.get('comment_type'),
1703 'text': self.request.POST.get('text'),
1703 'text': self.request.POST.get('text'),
1704 'status': self.request.POST.get('changeset_status', None),
1704 'status': self.request.POST.get('changeset_status', None),
1705 'is_draft': self.request.POST.get('draft'),
1705 'is_draft': self.request.POST.get('draft'),
1706 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1706 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1707 'close_pull_request': self.request.POST.get('close_pull_request'),
1707 'close_pull_request': self.request.POST.get('close_pull_request'),
1708 'f_path': self.request.POST.get('f_path'),
1708 'f_path': self.request.POST.get('f_path'),
1709 'line': self.request.POST.get('line'),
1709 'line': self.request.POST.get('line'),
1710 }
1710 }
1711 data = self._pull_request_comments_create(pull_request, [comment_data])
1711 data = self._pull_request_comments_create(pull_request, [comment_data])
1712
1712
1713 return data
1713 return data
1714
1714
1715 @LoginRequired()
1715 @LoginRequired()
1716 @NotAnonymous()
1716 @NotAnonymous()
1717 @HasRepoPermissionAnyDecorator(
1717 @HasRepoPermissionAnyDecorator(
1718 'repository.read', 'repository.write', 'repository.admin')
1718 'repository.read', 'repository.write', 'repository.admin')
1719 @CSRFRequired()
1719 @CSRFRequired()
1720 def pull_request_comment_delete(self):
1720 def pull_request_comment_delete(self):
1721 pull_request = PullRequest.get_or_404(
1721 pull_request = PullRequest.get_or_404(
1722 self.request.matchdict['pull_request_id'])
1722 self.request.matchdict['pull_request_id'])
1723
1723
1724 comment = ChangesetComment.get_or_404(
1724 comment = ChangesetComment.get_or_404(
1725 self.request.matchdict['comment_id'])
1725 self.request.matchdict['comment_id'])
1726 comment_id = comment.comment_id
1726 comment_id = comment.comment_id
1727
1727
1728 if comment.immutable:
1728 if comment.immutable:
1729 # don't allow deleting comments that are immutable
1729 # don't allow deleting comments that are immutable
1730 raise HTTPForbidden()
1730 raise HTTPForbidden()
1731
1731
1732 if pull_request.is_closed():
1732 if pull_request.is_closed():
1733 log.debug('comment: forbidden because pull request is closed')
1733 log.debug('comment: forbidden because pull request is closed')
1734 raise HTTPForbidden()
1734 raise HTTPForbidden()
1735
1735
1736 if not comment:
1736 if not comment:
1737 log.debug('Comment with id:%s not found, skipping', comment_id)
1737 log.debug('Comment with id:%s not found, skipping', comment_id)
1738 # comment already deleted in another call probably
1738 # comment already deleted in another call probably
1739 return True
1739 return True
1740
1740
1741 if comment.pull_request.is_closed():
1741 if comment.pull_request.is_closed():
1742 # don't allow deleting comments on closed pull request
1742 # don't allow deleting comments on closed pull request
1743 raise HTTPForbidden()
1743 raise HTTPForbidden()
1744
1744
1745 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1745 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1746 super_admin = h.HasPermissionAny('hg.admin')()
1746 super_admin = h.HasPermissionAny('hg.admin')()
1747 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1747 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1748 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1748 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1749 comment_repo_admin = is_repo_admin and is_repo_comment
1749 comment_repo_admin = is_repo_admin and is_repo_comment
1750
1750
1751 if comment.draft and not comment_owner:
1752 # We never allow to delete draft comments for other than owners
1753 raise HTTPNotFound()
1754
1751 if super_admin or comment_owner or comment_repo_admin:
1755 if super_admin or comment_owner or comment_repo_admin:
1752 old_calculated_status = comment.pull_request.calculated_review_status()
1756 old_calculated_status = comment.pull_request.calculated_review_status()
1753 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1757 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1754 Session().commit()
1758 Session().commit()
1755 calculated_status = comment.pull_request.calculated_review_status()
1759 calculated_status = comment.pull_request.calculated_review_status()
1756 if old_calculated_status != calculated_status:
1760 if old_calculated_status != calculated_status:
1757 PullRequestModel().trigger_pull_request_hook(
1761 PullRequestModel().trigger_pull_request_hook(
1758 comment.pull_request, self._rhodecode_user, 'review_status_change',
1762 comment.pull_request, self._rhodecode_user, 'review_status_change',
1759 data={'status': calculated_status})
1763 data={'status': calculated_status})
1760 return True
1764 return True
1761 else:
1765 else:
1762 log.warning('No permissions for user %s to delete comment_id: %s',
1766 log.warning('No permissions for user %s to delete comment_id: %s',
1763 self._rhodecode_db_user, comment_id)
1767 self._rhodecode_db_user, comment_id)
1764 raise HTTPNotFound()
1768 raise HTTPNotFound()
1765
1769
1766 @LoginRequired()
1770 @LoginRequired()
1767 @NotAnonymous()
1771 @NotAnonymous()
1768 @HasRepoPermissionAnyDecorator(
1772 @HasRepoPermissionAnyDecorator(
1769 'repository.read', 'repository.write', 'repository.admin')
1773 'repository.read', 'repository.write', 'repository.admin')
1770 @CSRFRequired()
1774 @CSRFRequired()
1771 def pull_request_comment_edit(self):
1775 def pull_request_comment_edit(self):
1772 self.load_default_context()
1776 self.load_default_context()
1773
1777
1774 pull_request = PullRequest.get_or_404(
1778 pull_request = PullRequest.get_or_404(
1775 self.request.matchdict['pull_request_id']
1779 self.request.matchdict['pull_request_id']
1776 )
1780 )
1777 comment = ChangesetComment.get_or_404(
1781 comment = ChangesetComment.get_or_404(
1778 self.request.matchdict['comment_id']
1782 self.request.matchdict['comment_id']
1779 )
1783 )
1780 comment_id = comment.comment_id
1784 comment_id = comment.comment_id
1781
1785
1782 if comment.immutable:
1786 if comment.immutable:
1783 # don't allow deleting comments that are immutable
1787 # don't allow deleting comments that are immutable
1784 raise HTTPForbidden()
1788 raise HTTPForbidden()
1785
1789
1786 if pull_request.is_closed():
1790 if pull_request.is_closed():
1787 log.debug('comment: forbidden because pull request is closed')
1791 log.debug('comment: forbidden because pull request is closed')
1788 raise HTTPForbidden()
1792 raise HTTPForbidden()
1789
1793
1790 if comment.pull_request.is_closed():
1794 if comment.pull_request.is_closed():
1791 # don't allow deleting comments on closed pull request
1795 # don't allow deleting comments on closed pull request
1792 raise HTTPForbidden()
1796 raise HTTPForbidden()
1793
1797
1794 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1798 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1795 super_admin = h.HasPermissionAny('hg.admin')()
1799 super_admin = h.HasPermissionAny('hg.admin')()
1796 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1800 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1797 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1801 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1798 comment_repo_admin = is_repo_admin and is_repo_comment
1802 comment_repo_admin = is_repo_admin and is_repo_comment
1799
1803
1800 if super_admin or comment_owner or comment_repo_admin:
1804 if super_admin or comment_owner or comment_repo_admin:
1801 text = self.request.POST.get('text')
1805 text = self.request.POST.get('text')
1802 version = self.request.POST.get('version')
1806 version = self.request.POST.get('version')
1803 if text == comment.text:
1807 if text == comment.text:
1804 log.warning(
1808 log.warning(
1805 'Comment(PR): '
1809 'Comment(PR): '
1806 'Trying to create new version '
1810 'Trying to create new version '
1807 'with the same comment body {}'.format(
1811 'with the same comment body {}'.format(
1808 comment_id,
1812 comment_id,
1809 )
1813 )
1810 )
1814 )
1811 raise HTTPNotFound()
1815 raise HTTPNotFound()
1812
1816
1813 if version.isdigit():
1817 if version.isdigit():
1814 version = int(version)
1818 version = int(version)
1815 else:
1819 else:
1816 log.warning(
1820 log.warning(
1817 'Comment(PR): Wrong version type {} {} '
1821 'Comment(PR): Wrong version type {} {} '
1818 'for comment {}'.format(
1822 'for comment {}'.format(
1819 version,
1823 version,
1820 type(version),
1824 type(version),
1821 comment_id,
1825 comment_id,
1822 )
1826 )
1823 )
1827 )
1824 raise HTTPNotFound()
1828 raise HTTPNotFound()
1825
1829
1826 try:
1830 try:
1827 comment_history = CommentsModel().edit(
1831 comment_history = CommentsModel().edit(
1828 comment_id=comment_id,
1832 comment_id=comment_id,
1829 text=text,
1833 text=text,
1830 auth_user=self._rhodecode_user,
1834 auth_user=self._rhodecode_user,
1831 version=version,
1835 version=version,
1832 )
1836 )
1833 except CommentVersionMismatch:
1837 except CommentVersionMismatch:
1834 raise HTTPConflict()
1838 raise HTTPConflict()
1835
1839
1836 if not comment_history:
1840 if not comment_history:
1837 raise HTTPNotFound()
1841 raise HTTPNotFound()
1838
1842
1839 Session().commit()
1843 Session().commit()
1840 if not comment.draft:
1844 if not comment.draft:
1841 PullRequestModel().trigger_pull_request_hook(
1845 PullRequestModel().trigger_pull_request_hook(
1842 pull_request, self._rhodecode_user, 'comment_edit',
1846 pull_request, self._rhodecode_user, 'comment_edit',
1843 data={'comment': comment})
1847 data={'comment': comment})
1844
1848
1845 return {
1849 return {
1846 'comment_history_id': comment_history.comment_history_id,
1850 'comment_history_id': comment_history.comment_history_id,
1847 'comment_id': comment.comment_id,
1851 'comment_id': comment.comment_id,
1848 'comment_version': comment_history.version,
1852 'comment_version': comment_history.version,
1849 'comment_author_username': comment_history.author.username,
1853 'comment_author_username': comment_history.author.username,
1850 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1854 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1851 'comment_created_on': h.age_component(comment_history.created_on,
1855 'comment_created_on': h.age_component(comment_history.created_on,
1852 time_is_local=True),
1856 time_is_local=True),
1853 }
1857 }
1854 else:
1858 else:
1855 log.warning('No permissions for user %s to edit comment_id: %s',
1859 log.warning('No permissions for user %s to edit comment_id: %s',
1856 self._rhodecode_db_user, comment_id)
1860 self._rhodecode_db_user, comment_id)
1857 raise HTTPNotFound()
1861 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now