##// END OF EJS Templates
fix(pull-requests): fixes for rendering comments
super-admin -
r5211:5e903185 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

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