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