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