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