##// END OF EJS Templates
pull-requests: add information about changes in source repositories in pull-request show page....
dan -
r4317:b8d0e5ed default
parent child
Show More
@@ -1,1506 +1,1508
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)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
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.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, EmptyRepositoryError)
44 RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 # backward compat., we use for OLD PRs a plain renderer
63 # backward compat., we use for OLD PRs a plain renderer
64 c.renderer = 'plain'
64 c.renderer = 'plain'
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, search_q=search_q, source=source, statuses=statuses,
83 repo_name, search_q=search_q, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, search_q=search_q, source=source, statuses=statuses,
100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.pull_request_state,
111 pr.pull_request_id, pr.pull_request_state,
112 pr.work_in_progress, pr.target_repo.repo_name),
112 pr.work_in_progress, pr.target_repo.repo_name),
113 'name_raw': pr.pull_request_id,
113 'name_raw': pr.pull_request_id,
114 'status': _render('pullrequest_status',
114 'status': _render('pullrequest_status',
115 pr.calculated_review_status()),
115 pr.calculated_review_status()),
116 'title': _render('pullrequest_title', pr.title, pr.description),
116 'title': _render('pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
117 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
118 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
119 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
121 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
122 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'state': pr.pull_request_state,
124 'state': pr.pull_request_state,
125 'author': _render('pullrequest_author',
125 'author': _render('pullrequest_author',
126 pr.author.full_contact, ),
126 pr.author.full_contact, ),
127 'author_raw': pr.author.full_name,
127 'author_raw': pr.author.full_name,
128 'comments': _render('pullrequest_comments', len(comments)),
128 'comments': _render('pullrequest_comments', len(comments)),
129 'comments_raw': len(comments),
129 'comments_raw': len(comments),
130 'closed': pr.is_closed(),
130 'closed': pr.is_closed(),
131 })
131 })
132
132
133 data = ({
133 data = ({
134 'draw': draw,
134 'draw': draw,
135 'data': data,
135 'data': data,
136 'recordsTotal': pull_requests_total_count,
136 'recordsTotal': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
138 })
138 })
139 return data
139 return data
140
140
141 @LoginRequired()
141 @LoginRequired()
142 @HasRepoPermissionAnyDecorator(
142 @HasRepoPermissionAnyDecorator(
143 'repository.read', 'repository.write', 'repository.admin')
143 'repository.read', 'repository.write', 'repository.admin')
144 @view_config(
144 @view_config(
145 route_name='pullrequest_show_all', request_method='GET',
145 route_name='pullrequest_show_all', request_method='GET',
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 def pull_request_list(self):
147 def pull_request_list(self):
148 c = self.load_default_context()
148 c = self.load_default_context()
149
149
150 req_get = self.request.GET
150 req_get = self.request.GET
151 c.source = str2bool(req_get.get('source'))
151 c.source = str2bool(req_get.get('source'))
152 c.closed = str2bool(req_get.get('closed'))
152 c.closed = str2bool(req_get.get('closed'))
153 c.my = str2bool(req_get.get('my'))
153 c.my = str2bool(req_get.get('my'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156
156
157 c.active = 'open'
157 c.active = 'open'
158 if c.my:
158 if c.my:
159 c.active = 'my'
159 c.active = 'my'
160 if c.closed:
160 if c.closed:
161 c.active = 'closed'
161 c.active = 'closed'
162 if c.awaiting_review and not c.source:
162 if c.awaiting_review and not c.source:
163 c.active = 'awaiting'
163 c.active = 'awaiting'
164 if c.source and not c.awaiting_review:
164 if c.source and not c.awaiting_review:
165 c.active = 'source'
165 c.active = 'source'
166 if c.awaiting_my_review:
166 if c.awaiting_my_review:
167 c.active = 'awaiting_my'
167 c.active = 'awaiting_my'
168
168
169 return self._get_template_context(c)
169 return self._get_template_context(c)
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @HasRepoPermissionAnyDecorator(
172 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
173 'repository.read', 'repository.write', 'repository.admin')
174 @view_config(
174 @view_config(
175 route_name='pullrequest_show_all_data', request_method='GET',
175 route_name='pullrequest_show_all_data', request_method='GET',
176 renderer='json_ext', xhr=True)
176 renderer='json_ext', xhr=True)
177 def pull_request_list_data(self):
177 def pull_request_list_data(self):
178 self.load_default_context()
178 self.load_default_context()
179
179
180 # additional filters
180 # additional filters
181 req_get = self.request.GET
181 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
182 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
183 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
184 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
187
188 filter_type = 'awaiting_review' if awaiting_review \
188 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
189 else 'awaiting_my_review' if awaiting_my_review \
190 else None
190 else None
191
191
192 opened_by = None
192 opened_by = None
193 if my:
193 if my:
194 opened_by = [self._rhodecode_user.user_id]
194 opened_by = [self._rhodecode_user.user_id]
195
195
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
197 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
198 statuses = [PullRequest.STATUS_CLOSED]
199
199
200 data = self._get_pull_requests_list(
200 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
201 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
203
204 return data
204 return data
205
205
206 def _is_diff_cache_enabled(self, target_repo):
206 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
207 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
208 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
209 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
210 return caching_enabled
211
211
212 def _get_diffset(self, source_repo_name, source_repo,
212 def _get_diffset(self, source_repo_name, source_repo,
213 source_ref_id, target_ref_id,
213 source_ref_id, target_ref_id,
214 target_commit, source_commit, diff_limit, file_limit,
214 target_commit, source_commit, diff_limit, file_limit,
215 fulldiff, hide_whitespace_changes, diff_context):
215 fulldiff, hide_whitespace_changes, diff_context):
216
216
217 vcs_diff = PullRequestModel().get_diff(
217 vcs_diff = PullRequestModel().get_diff(
218 source_repo, source_ref_id, target_ref_id,
218 source_repo, source_ref_id, target_ref_id,
219 hide_whitespace_changes, diff_context)
219 hide_whitespace_changes, diff_context)
220
220
221 diff_processor = diffs.DiffProcessor(
221 diff_processor = diffs.DiffProcessor(
222 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 vcs_diff, format='newdiff', diff_limit=diff_limit,
223 file_limit=file_limit, show_full_diff=fulldiff)
223 file_limit=file_limit, show_full_diff=fulldiff)
224
224
225 _parsed = diff_processor.prepare()
225 _parsed = diff_processor.prepare()
226
226
227 diffset = codeblocks.DiffSet(
227 diffset = codeblocks.DiffSet(
228 repo_name=self.db_repo_name,
228 repo_name=self.db_repo_name,
229 source_repo_name=source_repo_name,
229 source_repo_name=source_repo_name,
230 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 source_node_getter=codeblocks.diffset_node_getter(target_commit),
231 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 target_node_getter=codeblocks.diffset_node_getter(source_commit),
232 )
232 )
233 diffset = self.path_filter.render_patchset_filtered(
233 diffset = self.path_filter.render_patchset_filtered(
234 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
235
235
236 return diffset
236 return diffset
237
237
238 def _get_range_diffset(self, source_scm, source_repo,
238 def _get_range_diffset(self, source_scm, source_repo,
239 commit1, commit2, diff_limit, file_limit,
239 commit1, commit2, diff_limit, file_limit,
240 fulldiff, hide_whitespace_changes, diff_context):
240 fulldiff, hide_whitespace_changes, diff_context):
241 vcs_diff = source_scm.get_diff(
241 vcs_diff = source_scm.get_diff(
242 commit1, commit2,
242 commit1, commit2,
243 ignore_whitespace=hide_whitespace_changes,
243 ignore_whitespace=hide_whitespace_changes,
244 context=diff_context)
244 context=diff_context)
245
245
246 diff_processor = diffs.DiffProcessor(
246 diff_processor = diffs.DiffProcessor(
247 vcs_diff, format='newdiff', diff_limit=diff_limit,
247 vcs_diff, format='newdiff', diff_limit=diff_limit,
248 file_limit=file_limit, show_full_diff=fulldiff)
248 file_limit=file_limit, show_full_diff=fulldiff)
249
249
250 _parsed = diff_processor.prepare()
250 _parsed = diff_processor.prepare()
251
251
252 diffset = codeblocks.DiffSet(
252 diffset = codeblocks.DiffSet(
253 repo_name=source_repo.repo_name,
253 repo_name=source_repo.repo_name,
254 source_node_getter=codeblocks.diffset_node_getter(commit1),
254 source_node_getter=codeblocks.diffset_node_getter(commit1),
255 target_node_getter=codeblocks.diffset_node_getter(commit2))
255 target_node_getter=codeblocks.diffset_node_getter(commit2))
256
256
257 diffset = self.path_filter.render_patchset_filtered(
257 diffset = self.path_filter.render_patchset_filtered(
258 diffset, _parsed, commit1.raw_id, commit2.raw_id)
258 diffset, _parsed, commit1.raw_id, commit2.raw_id)
259
259
260 return diffset
260 return diffset
261
261
262 @LoginRequired()
262 @LoginRequired()
263 @HasRepoPermissionAnyDecorator(
263 @HasRepoPermissionAnyDecorator(
264 'repository.read', 'repository.write', 'repository.admin')
264 'repository.read', 'repository.write', 'repository.admin')
265 @view_config(
265 @view_config(
266 route_name='pullrequest_show', request_method='GET',
266 route_name='pullrequest_show', request_method='GET',
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
268 def pull_request_show(self):
268 def pull_request_show(self):
269 _ = self.request.translate
269 _ = self.request.translate
270 c = self.load_default_context()
270 c = self.load_default_context()
271
271
272 pull_request = PullRequest.get_or_404(
272 pull_request = PullRequest.get_or_404(
273 self.request.matchdict['pull_request_id'])
273 self.request.matchdict['pull_request_id'])
274 pull_request_id = pull_request.pull_request_id
274 pull_request_id = pull_request.pull_request_id
275
275
276 c.state_progressing = pull_request.is_state_changing()
276 c.state_progressing = pull_request.is_state_changing()
277
277
278 _new_state = {
278 _new_state = {
279 'created': PullRequest.STATE_CREATED,
279 'created': PullRequest.STATE_CREATED,
280 }.get(self.request.GET.get('force_state'))
280 }.get(self.request.GET.get('force_state'))
281 if c.is_super_admin and _new_state:
281 if c.is_super_admin and _new_state:
282 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
282 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
283 h.flash(
283 h.flash(
284 _('Pull Request state was force changed to `{}`').format(_new_state),
284 _('Pull Request state was force changed to `{}`').format(_new_state),
285 category='success')
285 category='success')
286 Session().commit()
286 Session().commit()
287
287
288 raise HTTPFound(h.route_path(
288 raise HTTPFound(h.route_path(
289 'pullrequest_show', repo_name=self.db_repo_name,
289 'pullrequest_show', repo_name=self.db_repo_name,
290 pull_request_id=pull_request_id))
290 pull_request_id=pull_request_id))
291
291
292 version = self.request.GET.get('version')
292 version = self.request.GET.get('version')
293 from_version = self.request.GET.get('from_version') or version
293 from_version = self.request.GET.get('from_version') or version
294 merge_checks = self.request.GET.get('merge_checks')
294 merge_checks = self.request.GET.get('merge_checks')
295 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
295 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
296
296
297 # fetch global flags of ignore ws or context lines
297 # fetch global flags of ignore ws or context lines
298 diff_context = diffs.get_diff_context(self.request)
298 diff_context = diffs.get_diff_context(self.request)
299 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
299 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
300
300
301 force_refresh = str2bool(self.request.GET.get('force_refresh'))
301 force_refresh = str2bool(self.request.GET.get('force_refresh'))
302
302
303 (pull_request_latest,
303 (pull_request_latest,
304 pull_request_at_ver,
304 pull_request_at_ver,
305 pull_request_display_obj,
305 pull_request_display_obj,
306 at_version) = PullRequestModel().get_pr_version(
306 at_version) = PullRequestModel().get_pr_version(
307 pull_request_id, version=version)
307 pull_request_id, version=version)
308 pr_closed = pull_request_latest.is_closed()
308 pr_closed = pull_request_latest.is_closed()
309
309
310 if pr_closed and (version or from_version):
310 if pr_closed and (version or from_version):
311 # not allow to browse versions
311 # not allow to browse versions
312 raise HTTPFound(h.route_path(
312 raise HTTPFound(h.route_path(
313 'pullrequest_show', repo_name=self.db_repo_name,
313 'pullrequest_show', repo_name=self.db_repo_name,
314 pull_request_id=pull_request_id))
314 pull_request_id=pull_request_id))
315
315
316 versions = pull_request_display_obj.versions()
316 versions = pull_request_display_obj.versions()
317 # used to store per-commit range diffs
317 # used to store per-commit range diffs
318 c.changes = collections.OrderedDict()
318 c.changes = collections.OrderedDict()
319 c.range_diff_on = self.request.GET.get('range-diff') == "1"
319 c.range_diff_on = self.request.GET.get('range-diff') == "1"
320
320
321 c.at_version = at_version
321 c.at_version = at_version
322 c.at_version_num = (at_version
322 c.at_version_num = (at_version
323 if at_version and at_version != 'latest'
323 if at_version and at_version != 'latest'
324 else None)
324 else None)
325 c.at_version_pos = ChangesetComment.get_index_from_version(
325 c.at_version_pos = ChangesetComment.get_index_from_version(
326 c.at_version_num, versions)
326 c.at_version_num, versions)
327
327
328 (prev_pull_request_latest,
328 (prev_pull_request_latest,
329 prev_pull_request_at_ver,
329 prev_pull_request_at_ver,
330 prev_pull_request_display_obj,
330 prev_pull_request_display_obj,
331 prev_at_version) = PullRequestModel().get_pr_version(
331 prev_at_version) = PullRequestModel().get_pr_version(
332 pull_request_id, version=from_version)
332 pull_request_id, version=from_version)
333
333
334 c.from_version = prev_at_version
334 c.from_version = prev_at_version
335 c.from_version_num = (prev_at_version
335 c.from_version_num = (prev_at_version
336 if prev_at_version and prev_at_version != 'latest'
336 if prev_at_version and prev_at_version != 'latest'
337 else None)
337 else None)
338 c.from_version_pos = ChangesetComment.get_index_from_version(
338 c.from_version_pos = ChangesetComment.get_index_from_version(
339 c.from_version_num, versions)
339 c.from_version_num, versions)
340
340
341 # define if we're in COMPARE mode or VIEW at version mode
341 # define if we're in COMPARE mode or VIEW at version mode
342 compare = at_version != prev_at_version
342 compare = at_version != prev_at_version
343
343
344 # pull_requests repo_name we opened it against
344 # pull_requests repo_name we opened it against
345 # ie. target_repo must match
345 # ie. target_repo must match
346 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
346 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
347 raise HTTPNotFound()
347 raise HTTPNotFound()
348
348
349 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
349 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
350 pull_request_at_ver)
350 pull_request_at_ver)
351
351
352 c.pull_request = pull_request_display_obj
352 c.pull_request = pull_request_display_obj
353 c.renderer = pull_request_at_ver.description_renderer or c.renderer
353 c.renderer = pull_request_at_ver.description_renderer or c.renderer
354 c.pull_request_latest = pull_request_latest
354 c.pull_request_latest = pull_request_latest
355
355
356 if compare or (at_version and not at_version == 'latest'):
356 if compare or (at_version and not at_version == 'latest'):
357 c.allowed_to_change_status = False
357 c.allowed_to_change_status = False
358 c.allowed_to_update = False
358 c.allowed_to_update = False
359 c.allowed_to_merge = False
359 c.allowed_to_merge = False
360 c.allowed_to_delete = False
360 c.allowed_to_delete = False
361 c.allowed_to_comment = False
361 c.allowed_to_comment = False
362 c.allowed_to_close = False
362 c.allowed_to_close = False
363 else:
363 else:
364 can_change_status = PullRequestModel().check_user_change_status(
364 can_change_status = PullRequestModel().check_user_change_status(
365 pull_request_at_ver, self._rhodecode_user)
365 pull_request_at_ver, self._rhodecode_user)
366 c.allowed_to_change_status = can_change_status and not pr_closed
366 c.allowed_to_change_status = can_change_status and not pr_closed
367
367
368 c.allowed_to_update = PullRequestModel().check_user_update(
368 c.allowed_to_update = PullRequestModel().check_user_update(
369 pull_request_latest, self._rhodecode_user) and not pr_closed
369 pull_request_latest, self._rhodecode_user) and not pr_closed
370 c.allowed_to_merge = PullRequestModel().check_user_merge(
370 c.allowed_to_merge = PullRequestModel().check_user_merge(
371 pull_request_latest, self._rhodecode_user) and not pr_closed
371 pull_request_latest, self._rhodecode_user) and not pr_closed
372 c.allowed_to_delete = PullRequestModel().check_user_delete(
372 c.allowed_to_delete = PullRequestModel().check_user_delete(
373 pull_request_latest, self._rhodecode_user) and not pr_closed
373 pull_request_latest, self._rhodecode_user) and not pr_closed
374 c.allowed_to_comment = not pr_closed
374 c.allowed_to_comment = not pr_closed
375 c.allowed_to_close = c.allowed_to_merge and not pr_closed
375 c.allowed_to_close = c.allowed_to_merge and not pr_closed
376
376
377 c.forbid_adding_reviewers = False
377 c.forbid_adding_reviewers = False
378 c.forbid_author_to_review = False
378 c.forbid_author_to_review = False
379 c.forbid_commit_author_to_review = False
379 c.forbid_commit_author_to_review = False
380
380
381 if pull_request_latest.reviewer_data and \
381 if pull_request_latest.reviewer_data and \
382 'rules' in pull_request_latest.reviewer_data:
382 'rules' in pull_request_latest.reviewer_data:
383 rules = pull_request_latest.reviewer_data['rules'] or {}
383 rules = pull_request_latest.reviewer_data['rules'] or {}
384 try:
384 try:
385 c.forbid_adding_reviewers = rules.get(
385 c.forbid_adding_reviewers = rules.get(
386 'forbid_adding_reviewers')
386 'forbid_adding_reviewers')
387 c.forbid_author_to_review = rules.get(
387 c.forbid_author_to_review = rules.get(
388 'forbid_author_to_review')
388 'forbid_author_to_review')
389 c.forbid_commit_author_to_review = rules.get(
389 c.forbid_commit_author_to_review = rules.get(
390 'forbid_commit_author_to_review')
390 'forbid_commit_author_to_review')
391 except Exception:
391 except Exception:
392 pass
392 pass
393
393
394 # check merge capabilities
394 # check merge capabilities
395 _merge_check = MergeCheck.validate(
395 _merge_check = MergeCheck.validate(
396 pull_request_latest, auth_user=self._rhodecode_user,
396 pull_request_latest, auth_user=self._rhodecode_user,
397 translator=self.request.translate,
397 translator=self.request.translate,
398 force_shadow_repo_refresh=force_refresh)
398 force_shadow_repo_refresh=force_refresh)
399
399
400 c.pr_merge_errors = _merge_check.error_details
400 c.pr_merge_errors = _merge_check.error_details
401 c.pr_merge_possible = not _merge_check.failed
401 c.pr_merge_possible = not _merge_check.failed
402 c.pr_merge_message = _merge_check.merge_msg
402 c.pr_merge_message = _merge_check.merge_msg
403 c.pr_merge_source_commit = _merge_check.source_commit
404 c.pr_merge_target_commit = _merge_check.target_commit
403
405
404 c.pr_merge_info = MergeCheck.get_merge_conditions(
406 c.pr_merge_info = MergeCheck.get_merge_conditions(
405 pull_request_latest, translator=self.request.translate)
407 pull_request_latest, translator=self.request.translate)
406
408
407 c.pull_request_review_status = _merge_check.review_status
409 c.pull_request_review_status = _merge_check.review_status
408 if merge_checks:
410 if merge_checks:
409 self.request.override_renderer = \
411 self.request.override_renderer = \
410 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
412 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
411 return self._get_template_context(c)
413 return self._get_template_context(c)
412
414
413 comments_model = CommentsModel()
415 comments_model = CommentsModel()
414
416
415 # reviewers and statuses
417 # reviewers and statuses
416 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
418 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
417 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
419 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
418
420
419 # GENERAL COMMENTS with versions #
421 # GENERAL COMMENTS with versions #
420 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
422 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
421 q = q.order_by(ChangesetComment.comment_id.asc())
423 q = q.order_by(ChangesetComment.comment_id.asc())
422 general_comments = q
424 general_comments = q
423
425
424 # pick comments we want to render at current version
426 # pick comments we want to render at current version
425 c.comment_versions = comments_model.aggregate_comments(
427 c.comment_versions = comments_model.aggregate_comments(
426 general_comments, versions, c.at_version_num)
428 general_comments, versions, c.at_version_num)
427 c.comments = c.comment_versions[c.at_version_num]['until']
429 c.comments = c.comment_versions[c.at_version_num]['until']
428
430
429 # INLINE COMMENTS with versions #
431 # INLINE COMMENTS with versions #
430 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
432 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
431 q = q.order_by(ChangesetComment.comment_id.asc())
433 q = q.order_by(ChangesetComment.comment_id.asc())
432 inline_comments = q
434 inline_comments = q
433
435
434 c.inline_versions = comments_model.aggregate_comments(
436 c.inline_versions = comments_model.aggregate_comments(
435 inline_comments, versions, c.at_version_num, inline=True)
437 inline_comments, versions, c.at_version_num, inline=True)
436
438
437 # TODOs
439 # TODOs
438 c.unresolved_comments = CommentsModel() \
440 c.unresolved_comments = CommentsModel() \
439 .get_pull_request_unresolved_todos(pull_request)
441 .get_pull_request_unresolved_todos(pull_request)
440 c.resolved_comments = CommentsModel() \
442 c.resolved_comments = CommentsModel() \
441 .get_pull_request_resolved_todos(pull_request)
443 .get_pull_request_resolved_todos(pull_request)
442
444
443 # inject latest version
445 # inject latest version
444 latest_ver = PullRequest.get_pr_display_object(
446 latest_ver = PullRequest.get_pr_display_object(
445 pull_request_latest, pull_request_latest)
447 pull_request_latest, pull_request_latest)
446
448
447 c.versions = versions + [latest_ver]
449 c.versions = versions + [latest_ver]
448
450
449 # if we use version, then do not show later comments
451 # if we use version, then do not show later comments
450 # than current version
452 # than current version
451 display_inline_comments = collections.defaultdict(
453 display_inline_comments = collections.defaultdict(
452 lambda: collections.defaultdict(list))
454 lambda: collections.defaultdict(list))
453 for co in inline_comments:
455 for co in inline_comments:
454 if c.at_version_num:
456 if c.at_version_num:
455 # pick comments that are at least UPTO given version, so we
457 # pick comments that are at least UPTO given version, so we
456 # don't render comments for higher version
458 # don't render comments for higher version
457 should_render = co.pull_request_version_id and \
459 should_render = co.pull_request_version_id and \
458 co.pull_request_version_id <= c.at_version_num
460 co.pull_request_version_id <= c.at_version_num
459 else:
461 else:
460 # showing all, for 'latest'
462 # showing all, for 'latest'
461 should_render = True
463 should_render = True
462
464
463 if should_render:
465 if should_render:
464 display_inline_comments[co.f_path][co.line_no].append(co)
466 display_inline_comments[co.f_path][co.line_no].append(co)
465
467
466 # load diff data into template context, if we use compare mode then
468 # load diff data into template context, if we use compare mode then
467 # diff is calculated based on changes between versions of PR
469 # diff is calculated based on changes between versions of PR
468
470
469 source_repo = pull_request_at_ver.source_repo
471 source_repo = pull_request_at_ver.source_repo
470 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
472 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
471
473
472 target_repo = pull_request_at_ver.target_repo
474 target_repo = pull_request_at_ver.target_repo
473 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
475 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
474
476
475 if compare:
477 if compare:
476 # in compare switch the diff base to latest commit from prev version
478 # in compare switch the diff base to latest commit from prev version
477 target_ref_id = prev_pull_request_display_obj.revisions[0]
479 target_ref_id = prev_pull_request_display_obj.revisions[0]
478
480
479 # despite opening commits for bookmarks/branches/tags, we always
481 # despite opening commits for bookmarks/branches/tags, we always
480 # convert this to rev to prevent changes after bookmark or branch change
482 # convert this to rev to prevent changes after bookmark or branch change
481 c.source_ref_type = 'rev'
483 c.source_ref_type = 'rev'
482 c.source_ref = source_ref_id
484 c.source_ref = source_ref_id
483
485
484 c.target_ref_type = 'rev'
486 c.target_ref_type = 'rev'
485 c.target_ref = target_ref_id
487 c.target_ref = target_ref_id
486
488
487 c.source_repo = source_repo
489 c.source_repo = source_repo
488 c.target_repo = target_repo
490 c.target_repo = target_repo
489
491
490 c.commit_ranges = []
492 c.commit_ranges = []
491 source_commit = EmptyCommit()
493 source_commit = EmptyCommit()
492 target_commit = EmptyCommit()
494 target_commit = EmptyCommit()
493 c.missing_requirements = False
495 c.missing_requirements = False
494
496
495 source_scm = source_repo.scm_instance()
497 source_scm = source_repo.scm_instance()
496 target_scm = target_repo.scm_instance()
498 target_scm = target_repo.scm_instance()
497
499
498 shadow_scm = None
500 shadow_scm = None
499 try:
501 try:
500 shadow_scm = pull_request_latest.get_shadow_repo()
502 shadow_scm = pull_request_latest.get_shadow_repo()
501 except Exception:
503 except Exception:
502 log.debug('Failed to get shadow repo', exc_info=True)
504 log.debug('Failed to get shadow repo', exc_info=True)
503 # try first the existing source_repo, and then shadow
505 # try first the existing source_repo, and then shadow
504 # repo if we can obtain one
506 # repo if we can obtain one
505 commits_source_repo = source_scm
507 commits_source_repo = source_scm
506 if shadow_scm:
508 if shadow_scm:
507 commits_source_repo = shadow_scm
509 commits_source_repo = shadow_scm
508
510
509 c.commits_source_repo = commits_source_repo
511 c.commits_source_repo = commits_source_repo
510 c.ancestor = None # set it to None, to hide it from PR view
512 c.ancestor = None # set it to None, to hide it from PR view
511
513
512 # empty version means latest, so we keep this to prevent
514 # empty version means latest, so we keep this to prevent
513 # double caching
515 # double caching
514 version_normalized = version or 'latest'
516 version_normalized = version or 'latest'
515 from_version_normalized = from_version or 'latest'
517 from_version_normalized = from_version or 'latest'
516
518
517 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
519 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
518 cache_file_path = diff_cache_exist(
520 cache_file_path = diff_cache_exist(
519 cache_path, 'pull_request', pull_request_id, version_normalized,
521 cache_path, 'pull_request', pull_request_id, version_normalized,
520 from_version_normalized, source_ref_id, target_ref_id,
522 from_version_normalized, source_ref_id, target_ref_id,
521 hide_whitespace_changes, diff_context, c.fulldiff)
523 hide_whitespace_changes, diff_context, c.fulldiff)
522
524
523 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
525 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
524 force_recache = self.get_recache_flag()
526 force_recache = self.get_recache_flag()
525
527
526 cached_diff = None
528 cached_diff = None
527 if caching_enabled:
529 if caching_enabled:
528 cached_diff = load_cached_diff(cache_file_path)
530 cached_diff = load_cached_diff(cache_file_path)
529
531
530 has_proper_commit_cache = (
532 has_proper_commit_cache = (
531 cached_diff and cached_diff.get('commits')
533 cached_diff and cached_diff.get('commits')
532 and len(cached_diff.get('commits', [])) == 5
534 and len(cached_diff.get('commits', [])) == 5
533 and cached_diff.get('commits')[0]
535 and cached_diff.get('commits')[0]
534 and cached_diff.get('commits')[3])
536 and cached_diff.get('commits')[3])
535
537
536 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
538 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
537 diff_commit_cache = \
539 diff_commit_cache = \
538 (ancestor_commit, commit_cache, missing_requirements,
540 (ancestor_commit, commit_cache, missing_requirements,
539 source_commit, target_commit) = cached_diff['commits']
541 source_commit, target_commit) = cached_diff['commits']
540 else:
542 else:
541 # NOTE(marcink): we reach potentially unreachable errors when a PR has
543 # NOTE(marcink): we reach potentially unreachable errors when a PR has
542 # merge errors resulting in potentially hidden commits in the shadow repo.
544 # merge errors resulting in potentially hidden commits in the shadow repo.
543 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
545 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
544 and _merge_check.merge_response
546 and _merge_check.merge_response
545 maybe_unreachable = maybe_unreachable \
547 maybe_unreachable = maybe_unreachable \
546 and _merge_check.merge_response.metadata.get('unresolved_files')
548 and _merge_check.merge_response.metadata.get('unresolved_files')
547 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
549 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
548 diff_commit_cache = \
550 diff_commit_cache = \
549 (ancestor_commit, commit_cache, missing_requirements,
551 (ancestor_commit, commit_cache, missing_requirements,
550 source_commit, target_commit) = self.get_commits(
552 source_commit, target_commit) = self.get_commits(
551 commits_source_repo,
553 commits_source_repo,
552 pull_request_at_ver,
554 pull_request_at_ver,
553 source_commit,
555 source_commit,
554 source_ref_id,
556 source_ref_id,
555 source_scm,
557 source_scm,
556 target_commit,
558 target_commit,
557 target_ref_id,
559 target_ref_id,
558 target_scm, maybe_unreachable=maybe_unreachable)
560 target_scm, maybe_unreachable=maybe_unreachable)
559
561
560 # register our commit range
562 # register our commit range
561 for comm in commit_cache.values():
563 for comm in commit_cache.values():
562 c.commit_ranges.append(comm)
564 c.commit_ranges.append(comm)
563
565
564 c.missing_requirements = missing_requirements
566 c.missing_requirements = missing_requirements
565 c.ancestor_commit = ancestor_commit
567 c.ancestor_commit = ancestor_commit
566 c.statuses = source_repo.statuses(
568 c.statuses = source_repo.statuses(
567 [x.raw_id for x in c.commit_ranges])
569 [x.raw_id for x in c.commit_ranges])
568
570
569 # auto collapse if we have more than limit
571 # auto collapse if we have more than limit
570 collapse_limit = diffs.DiffProcessor._collapse_commits_over
572 collapse_limit = diffs.DiffProcessor._collapse_commits_over
571 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
573 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
572 c.compare_mode = compare
574 c.compare_mode = compare
573
575
574 # diff_limit is the old behavior, will cut off the whole diff
576 # diff_limit is the old behavior, will cut off the whole diff
575 # if the limit is applied otherwise will just hide the
577 # if the limit is applied otherwise will just hide the
576 # big files from the front-end
578 # big files from the front-end
577 diff_limit = c.visual.cut_off_limit_diff
579 diff_limit = c.visual.cut_off_limit_diff
578 file_limit = c.visual.cut_off_limit_file
580 file_limit = c.visual.cut_off_limit_file
579
581
580 c.missing_commits = False