##// END OF EJS Templates
compare: fixed case of cross repo compare before links not working.
marcink -
r3146:8bba6dde default
parent child Browse files
Show More
@@ -1,305 +1,305 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib.helpers import _shorten_commit_id
24 24
25 25
26 26 def route_path(name, params=None, **kwargs):
27 27 import urllib
28 28
29 29 base_url = {
30 30 'repo_commit': '/{repo_name}/changeset/{commit_id}',
31 31 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
32 32 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
33 33 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
34 34 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
35 35 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
36 36 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
37 37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 38 }[name].format(**kwargs)
39 39
40 40 if params:
41 41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 42 return base_url
43 43
44 44
45 45 @pytest.mark.usefixtures("app")
46 46 class TestRepoCommitView(object):
47 47
48 48 def test_show_commit(self, backend):
49 49 commit_id = self.commit_id[backend.alias]
50 50 response = self.app.get(route_path(
51 51 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
52 52 response.mustcontain('Added a symlink')
53 53 response.mustcontain(commit_id)
54 54 response.mustcontain('No newline at end of file')
55 55
56 56 def test_show_raw(self, backend):
57 57 commit_id = self.commit_id[backend.alias]
58 58 response = self.app.get(route_path(
59 59 'repo_commit_raw',
60 60 repo_name=backend.repo_name, commit_id=commit_id))
61 61 assert response.body == self.diffs[backend.alias]
62 62
63 63 def test_show_raw_patch(self, backend):
64 64 response = self.app.get(route_path(
65 65 'repo_commit_patch', repo_name=backend.repo_name,
66 66 commit_id=self.commit_id[backend.alias]))
67 67 assert response.body == self.patches[backend.alias]
68 68
69 69 def test_commit_download(self, backend):
70 70 response = self.app.get(route_path(
71 71 'repo_commit_download',
72 72 repo_name=backend.repo_name,
73 73 commit_id=self.commit_id[backend.alias]))
74 74 assert response.body == self.diffs[backend.alias]
75 75
76 76 def test_single_commit_page_different_ops(self, backend):
77 77 commit_id = {
78 78 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
79 79 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
80 80 'svn': '337',
81 81 }
82 82 commit_id = commit_id[backend.alias]
83 83 response = self.app.get(route_path(
84 84 'repo_commit',
85 85 repo_name=backend.repo_name, commit_id=commit_id))
86 86
87 87 response.mustcontain(_shorten_commit_id(commit_id))
88 88 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
89 89
90 90 # files op files
91 response.mustcontain('File no longer present at commit: %s' %
91 response.mustcontain('File not present at commit: %s' %
92 92 _shorten_commit_id(commit_id))
93 93
94 94 # svn uses a different filename
95 95 if backend.alias == 'svn':
96 96 response.mustcontain('new file 10644')
97 97 else:
98 98 response.mustcontain('new file 100644')
99 99 response.mustcontain('Changed theme to ADC theme') # commit msg
100 100
101 101 self._check_new_diff_menus(response, right_menu=True)
102 102
103 103 def test_commit_range_page_different_ops(self, backend):
104 104 commit_id_range = {
105 105 'hg': (
106 106 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
107 107 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
108 108 'git': (
109 109 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
110 110 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
111 111 'svn': (
112 112 '335',
113 113 '337'),
114 114 }
115 115 commit_ids = commit_id_range[backend.alias]
116 116 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
117 117 response = self.app.get(route_path(
118 118 'repo_commit',
119 119 repo_name=backend.repo_name, commit_id=commit_id))
120 120
121 121 response.mustcontain(_shorten_commit_id(commit_ids[0]))
122 122 response.mustcontain(_shorten_commit_id(commit_ids[1]))
123 123
124 124 # svn is special
125 125 if backend.alias == 'svn':
126 126 response.mustcontain('new file 10644')
127 127 response.mustcontain('1 file changed: 5 inserted, 1 deleted')
128 128 response.mustcontain('12 files changed: 236 inserted, 22 deleted')
129 129 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
130 130 else:
131 131 response.mustcontain('new file 100644')
132 132 response.mustcontain('12 files changed: 222 inserted, 20 deleted')
133 133 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
134 134
135 135 # files op files
136 response.mustcontain('File no longer present at commit: %s' %
136 response.mustcontain('File not present at commit: %s' %
137 137 _shorten_commit_id(commit_ids[1]))
138 138 response.mustcontain('Added docstrings to vcs.cli') # commit msg
139 139 response.mustcontain('Changed theme to ADC theme') # commit msg
140 140
141 141 self._check_new_diff_menus(response)
142 142
143 143 def test_combined_compare_commit_page_different_ops(self, backend):
144 144 commit_id_range = {
145 145 'hg': (
146 146 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
147 147 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
148 148 'git': (
149 149 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
150 150 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
151 151 'svn': (
152 152 '335',
153 153 '337'),
154 154 }
155 155 commit_ids = commit_id_range[backend.alias]
156 156 response = self.app.get(route_path(
157 157 'repo_compare',
158 158 repo_name=backend.repo_name,
159 159 source_ref_type='rev', source_ref=commit_ids[0],
160 160 target_ref_type='rev', target_ref=commit_ids[1], ))
161 161
162 162 response.mustcontain(_shorten_commit_id(commit_ids[0]))
163 163 response.mustcontain(_shorten_commit_id(commit_ids[1]))
164 164
165 165 # files op files
166 response.mustcontain('File no longer present at commit: %s' %
166 response.mustcontain('File not present at commit: %s' %
167 167 _shorten_commit_id(commit_ids[1]))
168 168
169 169 # svn is special
170 170 if backend.alias == 'svn':
171 171 response.mustcontain('new file 10644')
172 172 response.mustcontain('32 files changed: 1179 inserted, 310 deleted')
173 173 else:
174 174 response.mustcontain('new file 100644')
175 175 response.mustcontain('32 files changed: 1165 inserted, 308 deleted')
176 176
177 177 response.mustcontain('Added docstrings to vcs.cli') # commit msg
178 178 response.mustcontain('Changed theme to ADC theme') # commit msg
179 179
180 180 self._check_new_diff_menus(response)
181 181
182 182 def test_changeset_range(self, backend):
183 183 self._check_changeset_range(
184 184 backend, self.commit_id_range, self.commit_id_range_result)
185 185
186 186 def test_changeset_range_with_initial_commit(self, backend):
187 187 commit_id_range = {
188 188 'hg': (
189 189 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
190 190 '...6cba7170863a2411822803fa77a0a264f1310b35'),
191 191 'git': (
192 192 'c1214f7e79e02fc37156ff215cd71275450cffc3'
193 193 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
194 194 'svn': '1...3',
195 195 }
196 196 commit_id_range_result = {
197 197 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
198 198 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
199 199 'svn': ['1', '2', '3'],
200 200 }
201 201 self._check_changeset_range(
202 202 backend, commit_id_range, commit_id_range_result)
203 203
204 204 def _check_changeset_range(
205 205 self, backend, commit_id_ranges, commit_id_range_result):
206 206 response = self.app.get(
207 207 route_path('repo_commit',
208 208 repo_name=backend.repo_name,
209 209 commit_id=commit_id_ranges[backend.alias]))
210 210
211 211 expected_result = commit_id_range_result[backend.alias]
212 212 response.mustcontain('{} commits'.format(len(expected_result)))
213 213 for commit_id in expected_result:
214 214 response.mustcontain(commit_id)
215 215
216 216 commit_id = {
217 217 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
218 218 'svn': '393',
219 219 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
220 220 }
221 221
222 222 commit_id_range = {
223 223 'hg': (
224 224 'a53d9201d4bc278910d416d94941b7ea007ecd52'
225 225 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
226 226 'git': (
227 227 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
228 228 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
229 229 'svn': '391...393',
230 230 }
231 231
232 232 commit_id_range_result = {
233 233 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
234 234 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
235 235 'svn': ['391', '392', '393'],
236 236 }
237 237
238 238 diffs = {
239 239 'hg': r"""diff --git a/README b/README
240 240 new file mode 120000
241 241 --- /dev/null
242 242 +++ b/README
243 243 @@ -0,0 +1,1 @@
244 244 +README.rst
245 245 \ No newline at end of file
246 246 """,
247 247 'git': r"""diff --git a/README b/README
248 248 new file mode 120000
249 249 index 0000000000000000000000000000000000000000..92cacd285355271487b7e379dba6ca60f9a554a4
250 250 --- /dev/null
251 251 +++ b/README
252 252 @@ -0,0 +1 @@
253 253 +README.rst
254 254 \ No newline at end of file
255 255 """,
256 256 'svn': """Index: README
257 257 ===================================================================
258 258 diff --git a/README b/README
259 259 new file mode 10644
260 260 --- /dev/null\t(revision 0)
261 261 +++ b/README\t(revision 393)
262 262 @@ -0,0 +1 @@
263 263 +link README.rst
264 264 \\ No newline at end of file
265 265 """,
266 266 }
267 267
268 268 patches = {
269 269 'hg': r"""# HG changeset patch
270 270 # User Marcin Kuzminski <marcin@python-works.com>
271 271 # Date 2014-01-07 12:21:40
272 272 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
273 273 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
274 274
275 275 Added a symlink
276 276
277 277 """ + diffs['hg'],
278 278 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
279 279 From: Marcin Kuzminski <marcin@python-works.com>
280 280 Date: 2014-01-07 12:22:20
281 281 Subject: [PATCH] Added a symlink
282 282
283 283 ---
284 284
285 285 """ + diffs['git'],
286 286 'svn': r"""# SVN changeset patch
287 287 # User marcin
288 288 # Date 2014-09-02 12:25:22.071142
289 289 # Revision 393
290 290
291 291 Added a symlink
292 292
293 293 """ + diffs['svn'],
294 294 }
295 295
296 296 def _check_new_diff_menus(self, response, right_menu=False,):
297 297 # individual file diff menus
298 298 for elem in ['Show file before', 'Show file after']:
299 299 response.mustcontain(elem)
300 300
301 301 # right pane diff menus
302 302 if right_menu:
303 303 for elem in ['Hide whitespace changes', 'Toggle Wide Mode diff',
304 304 'Show full context diff']:
305 305 response.mustcontain(elem)
@@ -1,315 +1,316 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 25 from pyramid.view import view_config
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from rhodecode.lib.utils import safe_str
35 35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 36 from rhodecode.lib.vcs.exceptions import (
37 37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 38 NodeDoesNotExistError)
39 39 from rhodecode.model.db import Repository, ChangesetStatus
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class RepoCompareView(RepoAppView):
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context(include_app_defaults=True)
47 47 c.rhodecode_repo = self.rhodecode_vcs_repo
48 48 return c
49 49
50 50 def _get_commit_or_redirect(
51 51 self, ref, ref_type, repo, redirect_after=True, partial=False):
52 52 """
53 53 This is a safe way to get a commit. If an error occurs it
54 54 redirects to a commit with a proper message. If partial is set
55 55 then it does not do redirect raise and throws an exception instead.
56 56 """
57 57 _ = self.request.translate
58 58 try:
59 59 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
60 60 except EmptyRepositoryError:
61 61 if not redirect_after:
62 62 return repo.scm_instance().EMPTY_COMMIT
63 63 h.flash(h.literal(_('There are no commits yet')),
64 64 category='warning')
65 65 if not partial:
66 66 raise HTTPFound(
67 67 h.route_path('repo_summary', repo_name=repo.repo_name))
68 68 raise HTTPBadRequest()
69 69
70 70 except RepositoryError as e:
71 71 log.exception(safe_str(e))
72 72 h.flash(safe_str(h.escape(e)), category='warning')
73 73 if not partial:
74 74 raise HTTPFound(
75 75 h.route_path('repo_summary', repo_name=repo.repo_name))
76 76 raise HTTPBadRequest()
77 77
78 78 @LoginRequired()
79 79 @HasRepoPermissionAnyDecorator(
80 80 'repository.read', 'repository.write', 'repository.admin')
81 81 @view_config(
82 82 route_name='repo_compare_select', request_method='GET',
83 83 renderer='rhodecode:templates/compare/compare_diff.mako')
84 84 def compare_select(self):
85 85 _ = self.request.translate
86 86 c = self.load_default_context()
87 87
88 88 source_repo = self.db_repo_name
89 89 target_repo = self.request.GET.get('target_repo', source_repo)
90 90 c.source_repo = Repository.get_by_repo_name(source_repo)
91 91 c.target_repo = Repository.get_by_repo_name(target_repo)
92 92
93 93 if c.source_repo is None or c.target_repo is None:
94 94 raise HTTPNotFound()
95 95
96 96 c.compare_home = True
97 97 c.commit_ranges = []
98 98 c.collapse_all_commits = False
99 99 c.diffset = None
100 100 c.limited_diff = False
101 101 c.source_ref = c.target_ref = _('Select commit')
102 102 c.source_ref_type = ""
103 103 c.target_ref_type = ""
104 104 c.commit_statuses = ChangesetStatus.STATUSES
105 105 c.preview_mode = False
106 106 c.file_path = None
107 107
108 108 return self._get_template_context(c)
109 109
110 110 @LoginRequired()
111 111 @HasRepoPermissionAnyDecorator(
112 112 'repository.read', 'repository.write', 'repository.admin')
113 113 @view_config(
114 114 route_name='repo_compare', request_method='GET',
115 115 renderer=None)
116 116 def compare(self):
117 117 _ = self.request.translate
118 118 c = self.load_default_context()
119 119
120 120 source_ref_type = self.request.matchdict['source_ref_type']
121 121 source_ref = self.request.matchdict['source_ref']
122 122 target_ref_type = self.request.matchdict['target_ref_type']
123 123 target_ref = self.request.matchdict['target_ref']
124 124
125 125 # source_ref will be evaluated in source_repo
126 126 source_repo_name = self.db_repo_name
127 127 source_path, source_id = parse_path_ref(source_ref)
128 128
129 129 # target_ref will be evaluated in target_repo
130 130 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
131 131 target_path, target_id = parse_path_ref(
132 132 target_ref, default_path=self.request.GET.get('f_path', ''))
133 133
134 134 # if merge is True
135 135 # Show what changes since the shared ancestor commit of target/source
136 136 # the source would get if it was merged with target. Only commits
137 137 # which are in target but not in source will be shown.
138 138 merge = str2bool(self.request.GET.get('merge'))
139 139 # if merge is False
140 140 # Show a raw diff of source/target refs even if no ancestor exists
141 141
142 142 # c.fulldiff disables cut_off_limit
143 143 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
144 144
145 145 # fetch global flags of ignore ws or context lines
146 146 diff_context = diffs.get_diff_context(self.request)
147 147 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
148 148
149 149 c.file_path = target_path
150 150 c.commit_statuses = ChangesetStatus.STATUSES
151 151
152 152 # if partial, returns just compare_commits.html (commits log)
153 153 partial = self.request.is_xhr
154 154
155 155 # swap url for compare_diff page
156 156 c.swap_url = h.route_path(
157 157 'repo_compare',
158 158 repo_name=target_repo_name,
159 159 source_ref_type=target_ref_type,
160 160 source_ref=target_ref,
161 161 target_repo=source_repo_name,
162 162 target_ref_type=source_ref_type,
163 163 target_ref=source_ref,
164 164 _query=dict(merge=merge and '1' or '', f_path=target_path))
165 165
166 166 source_repo = Repository.get_by_repo_name(source_repo_name)
167 167 target_repo = Repository.get_by_repo_name(target_repo_name)
168 168
169 169 if source_repo is None:
170 170 log.error('Could not find the source repo: {}'
171 171 .format(source_repo_name))
172 172 h.flash(_('Could not find the source repo: `{}`')
173 173 .format(h.escape(source_repo_name)), category='error')
174 174 raise HTTPFound(
175 175 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
176 176
177 177 if target_repo is None:
178 178 log.error('Could not find the target repo: {}'
179 179 .format(source_repo_name))
180 180 h.flash(_('Could not find the target repo: `{}`')
181 181 .format(h.escape(target_repo_name)), category='error')
182 182 raise HTTPFound(
183 183 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
184 184
185 185 source_scm = source_repo.scm_instance()
186 186 target_scm = target_repo.scm_instance()
187 187
188 188 source_alias = source_scm.alias
189 189 target_alias = target_scm.alias
190 190 if source_alias != target_alias:
191 191 msg = _('The comparison of two different kinds of remote repos '
192 192 'is not available')
193 193 log.error(msg)
194 194 h.flash(msg, category='error')
195 195 raise HTTPFound(
196 196 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
197 197
198 198 source_commit = self._get_commit_or_redirect(
199 199 ref=source_id, ref_type=source_ref_type, repo=source_repo,
200 200 partial=partial)
201 201 target_commit = self._get_commit_or_redirect(
202 202 ref=target_id, ref_type=target_ref_type, repo=target_repo,
203 203 partial=partial)
204 204
205 205 c.compare_home = False
206 206 c.source_repo = source_repo
207 207 c.target_repo = target_repo
208 208 c.source_ref = source_ref
209 209 c.target_ref = target_ref
210 210 c.source_ref_type = source_ref_type
211 211 c.target_ref_type = target_ref_type
212 212
213 213 pre_load = ["author", "branch", "date", "message"]
214 214 c.ancestor = None
215 215
216 216 if c.file_path:
217 217 if source_commit == target_commit:
218 218 c.commit_ranges = []
219 219 else:
220 220 c.commit_ranges = [target_commit]
221 221 else:
222 222 try:
223 223 c.commit_ranges = source_scm.compare(
224 224 source_commit.raw_id, target_commit.raw_id,
225 225 target_scm, merge, pre_load=pre_load)
226 226 if merge:
227 227 c.ancestor = source_scm.get_common_ancestor(
228 228 source_commit.raw_id, target_commit.raw_id, target_scm)
229 229 except RepositoryRequirementError:
230 230 msg = _('Could not compare repos with different '
231 231 'large file settings')
232 232 log.error(msg)
233 233 if partial:
234 234 return Response(msg)
235 235 h.flash(msg, category='error')
236 236 raise HTTPFound(
237 237 h.route_path('repo_compare_select',
238 238 repo_name=self.db_repo_name))
239 239
240 240 c.statuses = self.db_repo.statuses(
241 241 [x.raw_id for x in c.commit_ranges])
242 242
243 243 # auto collapse if we have more than limit
244 244 collapse_limit = diffs.DiffProcessor._collapse_commits_over
245 245 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
246 246
247 247 if partial: # for PR ajax commits loader
248 248 if not c.ancestor:
249 249 return Response('') # cannot merge if there is no ancestor
250 250
251 251 html = render(
252 252 'rhodecode:templates/compare/compare_commits.mako',
253 253 self._get_template_context(c), self.request)
254 254 return Response(html)
255 255
256 256 if c.ancestor:
257 257 # case we want a simple diff without incoming commits,
258 258 # previewing what will be merged.
259 259 # Make the diff on target repo (which is known to have target_ref)
260 260 log.debug('Using ancestor %s as source_ref instead of %s',
261 261 c.ancestor, source_ref)
262 262 source_repo = target_repo
263 263 source_commit = target_repo.get_commit(commit_id=c.ancestor)
264 264
265 265 # diff_limit will cut off the whole diff if the limit is applied
266 266 # otherwise it will just hide the big files from the front-end
267 267 diff_limit = c.visual.cut_off_limit_diff
268 268 file_limit = c.visual.cut_off_limit_file
269 269
270 270 log.debug('calculating diff between '
271 271 'source_ref:%s and target_ref:%s for repo `%s`',
272 272 source_commit, target_commit,
273 273 safe_unicode(source_repo.scm_instance().path))
274 274
275 275 if source_commit.repository != target_commit.repository:
276 276 msg = _(
277 277 "Repositories unrelated. "
278 278 "Cannot compare commit %(commit1)s from repository %(repo1)s "
279 279 "with commit %(commit2)s from repository %(repo2)s.") % {
280 280 'commit1': h.show_id(source_commit),
281 281 'repo1': source_repo.repo_name,
282 282 'commit2': h.show_id(target_commit),
283 283 'repo2': target_repo.repo_name,
284 284 }
285 285 h.flash(msg, category='error')
286 286 raise HTTPFound(
287 287 h.route_path('repo_compare_select',
288 288 repo_name=self.db_repo_name))
289 289
290 290 txt_diff = source_repo.scm_instance().get_diff(
291 291 commit1=source_commit, commit2=target_commit,
292 292 path=target_path, path1=source_path,
293 293 ignore_whitespace=hide_whitespace_changes, context=diff_context)
294 294
295 295 diff_processor = diffs.DiffProcessor(
296 296 txt_diff, format='newdiff', diff_limit=diff_limit,
297 297 file_limit=file_limit, show_full_diff=c.fulldiff)
298 298 _parsed = diff_processor.prepare()
299 299
300 300 diffset = codeblocks.DiffSet(
301 301 repo_name=source_repo.repo_name,
302 302 source_node_getter=codeblocks.diffset_node_getter(source_commit),
303 target_repo_name=self.db_repo_name,
303 304 target_node_getter=codeblocks.diffset_node_getter(target_commit),
304 305 )
305 306 c.diffset = self.path_filter.render_patchset_filtered(
306 307 diffset, _parsed, source_ref, target_ref)
307 308
308 309 c.preview_mode = merge
309 310 c.source_commit = source_commit
310 311 c.target_commit = target_commit
311 312
312 313 html = render(
313 314 'rhodecode:templates/compare/compare_diff.mako',
314 315 self._get_template_context(c), self.request)
315 316 return Response(html) No newline at end of file
@@ -1,771 +1,775 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import difflib
23 23 from itertools import groupby
24 24
25 25 from pygments import lex
26 26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 27 from pygments.lexers.special import TextLexer, Token
28 28 from pygments.lexers import get_lexer_by_name
29 29
30 30 from rhodecode.lib.helpers import (
31 31 get_lexer_for_filenode, html_escape, get_custom_lexer)
32 32 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
33 33 from rhodecode.lib.vcs.nodes import FileNode
34 34 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
35 35 from rhodecode.lib.diff_match_patch import diff_match_patch
36 36 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
37 37
38 38
39 39 plain_text_lexer = get_lexer_by_name(
40 40 'text', stripall=False, stripnl=False, ensurenl=False)
41 41
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 def filenode_as_lines_tokens(filenode, lexer=None):
47 47 org_lexer = lexer
48 48 lexer = lexer or get_lexer_for_filenode(filenode)
49 49 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
50 50 lexer, filenode, org_lexer)
51 51 tokens = tokenize_string(filenode.content, lexer)
52 52 lines = split_token_stream(tokens)
53 53 rv = list(lines)
54 54 return rv
55 55
56 56
57 57 def tokenize_string(content, lexer):
58 58 """
59 59 Use pygments to tokenize some content based on a lexer
60 60 ensuring all original new lines and whitespace is preserved
61 61 """
62 62
63 63 lexer.stripall = False
64 64 lexer.stripnl = False
65 65 lexer.ensurenl = False
66 66
67 67 if isinstance(lexer, TextLexer):
68 68 lexed = [(Token.Text, content)]
69 69 else:
70 70 lexed = lex(content, lexer)
71 71
72 72 for token_type, token_text in lexed:
73 73 yield pygment_token_class(token_type), token_text
74 74
75 75
76 76 def split_token_stream(tokens):
77 77 """
78 78 Take a list of (TokenType, text) tuples and split them by a string
79 79
80 80 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
81 81 [(TEXT, 'some'), (TEXT, 'text'),
82 82 (TEXT, 'more'), (TEXT, 'text')]
83 83 """
84 84
85 85 buffer = []
86 86 for token_class, token_text in tokens:
87 87 parts = token_text.split('\n')
88 88 for part in parts[:-1]:
89 89 buffer.append((token_class, part))
90 90 yield buffer
91 91 buffer = []
92 92
93 93 buffer.append((token_class, parts[-1]))
94 94
95 95 if buffer:
96 96 yield buffer
97 97
98 98
99 99 def filenode_as_annotated_lines_tokens(filenode):
100 100 """
101 101 Take a file node and return a list of annotations => lines, if no annotation
102 102 is found, it will be None.
103 103
104 104 eg:
105 105
106 106 [
107 107 (annotation1, [
108 108 (1, line1_tokens_list),
109 109 (2, line2_tokens_list),
110 110 ]),
111 111 (annotation2, [
112 112 (3, line1_tokens_list),
113 113 ]),
114 114 (None, [
115 115 (4, line1_tokens_list),
116 116 ]),
117 117 (annotation1, [
118 118 (5, line1_tokens_list),
119 119 (6, line2_tokens_list),
120 120 ])
121 121 ]
122 122 """
123 123
124 124 commit_cache = {} # cache commit_getter lookups
125 125
126 126 def _get_annotation(commit_id, commit_getter):
127 127 if commit_id not in commit_cache:
128 128 commit_cache[commit_id] = commit_getter()
129 129 return commit_cache[commit_id]
130 130
131 131 annotation_lookup = {
132 132 line_no: _get_annotation(commit_id, commit_getter)
133 133 for line_no, commit_id, commit_getter, line_content
134 134 in filenode.annotate
135 135 }
136 136
137 137 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
138 138 for line_no, tokens
139 139 in enumerate(filenode_as_lines_tokens(filenode), 1))
140 140
141 141 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
142 142
143 143 for annotation, group in grouped_annotations_lines:
144 144 yield (
145 145 annotation, [(line_no, tokens)
146 146 for (_, line_no, tokens) in group]
147 147 )
148 148
149 149
150 150 def render_tokenstream(tokenstream):
151 151 result = []
152 152 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
153 153
154 154 if token_class:
155 155 result.append(u'<span class="%s">' % token_class)
156 156 else:
157 157 result.append(u'<span>')
158 158
159 159 for op_tag, token_text in token_ops_texts:
160 160
161 161 if op_tag:
162 162 result.append(u'<%s>' % op_tag)
163 163
164 164 escaped_text = html_escape(token_text)
165 165
166 166 # TODO: dan: investigate showing hidden characters like space/nl/tab
167 167 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
168 168 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
169 169 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
170 170
171 171 result.append(escaped_text)
172 172
173 173 if op_tag:
174 174 result.append(u'</%s>' % op_tag)
175 175
176 176 result.append(u'</span>')
177 177
178 178 html = ''.join(result)
179 179 return html
180 180
181 181
182 182 def rollup_tokenstream(tokenstream):
183 183 """
184 184 Group a token stream of the format:
185 185
186 186 ('class', 'op', 'text')
187 187 or
188 188 ('class', 'text')
189 189
190 190 into
191 191
192 192 [('class1',
193 193 [('op1', 'text'),
194 194 ('op2', 'text')]),
195 195 ('class2',
196 196 [('op3', 'text')])]
197 197
198 198 This is used to get the minimal tags necessary when
199 199 rendering to html eg for a token stream ie.
200 200
201 201 <span class="A"><ins>he</ins>llo</span>
202 202 vs
203 203 <span class="A"><ins>he</ins></span><span class="A">llo</span>
204 204
205 205 If a 2 tuple is passed in, the output op will be an empty string.
206 206
207 207 eg:
208 208
209 209 >>> rollup_tokenstream([('classA', '', 'h'),
210 210 ('classA', 'del', 'ell'),
211 211 ('classA', '', 'o'),
212 212 ('classB', '', ' '),
213 213 ('classA', '', 'the'),
214 214 ('classA', '', 're'),
215 215 ])
216 216
217 217 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
218 218 ('classB', [('', ' ')],
219 219 ('classA', [('', 'there')]]
220 220
221 221 """
222 222 if tokenstream and len(tokenstream[0]) == 2:
223 223 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
224 224
225 225 result = []
226 226 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
227 227 ops = []
228 228 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
229 229 text_buffer = []
230 230 for t_class, t_op, t_text in token_text_list:
231 231 text_buffer.append(t_text)
232 232 ops.append((token_op, ''.join(text_buffer)))
233 233 result.append((token_class, ops))
234 234 return result
235 235
236 236
237 237 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
238 238 """
239 239 Converts a list of (token_class, token_text) tuples to a list of
240 240 (token_class, token_op, token_text) tuples where token_op is one of
241 241 ('ins', 'del', '')
242 242
243 243 :param old_tokens: list of (token_class, token_text) tuples of old line
244 244 :param new_tokens: list of (token_class, token_text) tuples of new line
245 245 :param use_diff_match_patch: boolean, will use google's diff match patch
246 246 library which has options to 'smooth' out the character by character
247 247 differences making nicer ins/del blocks
248 248 """
249 249
250 250 old_tokens_result = []
251 251 new_tokens_result = []
252 252
253 253 similarity = difflib.SequenceMatcher(None,
254 254 ''.join(token_text for token_class, token_text in old_tokens),
255 255 ''.join(token_text for token_class, token_text in new_tokens)
256 256 ).ratio()
257 257
258 258 if similarity < 0.6: # return, the blocks are too different
259 259 for token_class, token_text in old_tokens:
260 260 old_tokens_result.append((token_class, '', token_text))
261 261 for token_class, token_text in new_tokens:
262 262 new_tokens_result.append((token_class, '', token_text))
263 263 return old_tokens_result, new_tokens_result, similarity
264 264
265 265 token_sequence_matcher = difflib.SequenceMatcher(None,
266 266 [x[1] for x in old_tokens],
267 267 [x[1] for x in new_tokens])
268 268
269 269 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
270 270 # check the differences by token block types first to give a more
271 271 # nicer "block" level replacement vs character diffs
272 272
273 273 if tag == 'equal':
274 274 for token_class, token_text in old_tokens[o1:o2]:
275 275 old_tokens_result.append((token_class, '', token_text))
276 276 for token_class, token_text in new_tokens[n1:n2]:
277 277 new_tokens_result.append((token_class, '', token_text))
278 278 elif tag == 'delete':
279 279 for token_class, token_text in old_tokens[o1:o2]:
280 280 old_tokens_result.append((token_class, 'del', token_text))
281 281 elif tag == 'insert':
282 282 for token_class, token_text in new_tokens[n1:n2]:
283 283 new_tokens_result.append((token_class, 'ins', token_text))
284 284 elif tag == 'replace':
285 285 # if same type token blocks must be replaced, do a diff on the
286 286 # characters in the token blocks to show individual changes
287 287
288 288 old_char_tokens = []
289 289 new_char_tokens = []
290 290 for token_class, token_text in old_tokens[o1:o2]:
291 291 for char in token_text:
292 292 old_char_tokens.append((token_class, char))
293 293
294 294 for token_class, token_text in new_tokens[n1:n2]:
295 295 for char in token_text:
296 296 new_char_tokens.append((token_class, char))
297 297
298 298 old_string = ''.join([token_text for
299 299 token_class, token_text in old_char_tokens])
300 300 new_string = ''.join([token_text for
301 301 token_class, token_text in new_char_tokens])
302 302
303 303 char_sequence = difflib.SequenceMatcher(
304 304 None, old_string, new_string)
305 305 copcodes = char_sequence.get_opcodes()
306 306 obuffer, nbuffer = [], []
307 307
308 308 if use_diff_match_patch:
309 309 dmp = diff_match_patch()
310 310 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
311 311 reps = dmp.diff_main(old_string, new_string)
312 312 dmp.diff_cleanupEfficiency(reps)
313 313
314 314 a, b = 0, 0
315 315 for op, rep in reps:
316 316 l = len(rep)
317 317 if op == 0:
318 318 for i, c in enumerate(rep):
319 319 obuffer.append((old_char_tokens[a+i][0], '', c))
320 320 nbuffer.append((new_char_tokens[b+i][0], '', c))
321 321 a += l
322 322 b += l
323 323 elif op == -1:
324 324 for i, c in enumerate(rep):
325 325 obuffer.append((old_char_tokens[a+i][0], 'del', c))
326 326 a += l
327 327 elif op == 1:
328 328 for i, c in enumerate(rep):
329 329 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
330 330 b += l
331 331 else:
332 332 for ctag, co1, co2, cn1, cn2 in copcodes:
333 333 if ctag == 'equal':
334 334 for token_class, token_text in old_char_tokens[co1:co2]:
335 335 obuffer.append((token_class, '', token_text))
336 336 for token_class, token_text in new_char_tokens[cn1:cn2]:
337 337 nbuffer.append((token_class, '', token_text))
338 338 elif ctag == 'delete':
339 339 for token_class, token_text in old_char_tokens[co1:co2]:
340 340 obuffer.append((token_class, 'del', token_text))
341 341 elif ctag == 'insert':
342 342 for token_class, token_text in new_char_tokens[cn1:cn2]:
343 343 nbuffer.append((token_class, 'ins', token_text))
344 344 elif ctag == 'replace':
345 345 for token_class, token_text in old_char_tokens[co1:co2]:
346 346 obuffer.append((token_class, 'del', token_text))
347 347 for token_class, token_text in new_char_tokens[cn1:cn2]:
348 348 nbuffer.append((token_class, 'ins', token_text))
349 349
350 350 old_tokens_result.extend(obuffer)
351 351 new_tokens_result.extend(nbuffer)
352 352
353 353 return old_tokens_result, new_tokens_result, similarity
354 354
355 355
356 356 def diffset_node_getter(commit):
357 357 def get_node(fname):
358 358 try:
359 359 return commit.get_node(fname)
360 360 except NodeDoesNotExistError:
361 361 return None
362 362
363 363 return get_node
364 364
365 365
366 366 class DiffSet(object):
367 367 """
368 368 An object for parsing the diff result from diffs.DiffProcessor and
369 369 adding highlighting, side by side/unified renderings and line diffs
370 370 """
371 371
372 372 HL_REAL = 'REAL' # highlights using original file, slow
373 373 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
374 374 # in the case of multiline code
375 375 HL_NONE = 'NONE' # no highlighting, fastest
376 376
377 377 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
378 378 source_repo_name=None,
379 379 source_node_getter=lambda filename: None,
380 target_repo_name=None,
380 381 target_node_getter=lambda filename: None,
381 382 source_nodes=None, target_nodes=None,
382 383 # files over this size will use fast highlighting
383 384 max_file_size_limit=150 * 1024,
384 385 ):
385 386
386 387 self.highlight_mode = highlight_mode
387 388 self.highlighted_filenodes = {}
388 389 self.source_node_getter = source_node_getter
389 390 self.target_node_getter = target_node_getter
390 391 self.source_nodes = source_nodes or {}
391 392 self.target_nodes = target_nodes or {}
392 393 self.repo_name = repo_name
394 self.target_repo_name = target_repo_name or repo_name
393 395 self.source_repo_name = source_repo_name or repo_name
394 396 self.max_file_size_limit = max_file_size_limit
395 397
396 398 def render_patchset(self, patchset, source_ref=None, target_ref=None):
397 399 diffset = AttributeDict(dict(
398 400 lines_added=0,
399 401 lines_deleted=0,
400 402 changed_files=0,
401 403 files=[],
402 404 file_stats={},
403 405 limited_diff=isinstance(patchset, LimitedDiffContainer),
404 406 repo_name=self.repo_name,
407 target_repo_name=self.target_repo_name,
405 408 source_repo_name=self.source_repo_name,
406 409 source_ref=source_ref,
407 410 target_ref=target_ref,
408 411 ))
409 412 for patch in patchset:
410 413 diffset.file_stats[patch['filename']] = patch['stats']
411 414 filediff = self.render_patch(patch)
412 415 filediff.diffset = StrictAttributeDict(dict(
413 416 source_ref=diffset.source_ref,
414 417 target_ref=diffset.target_ref,
415 418 repo_name=diffset.repo_name,
416 419 source_repo_name=diffset.source_repo_name,
420 target_repo_name=diffset.target_repo_name,
417 421 ))
418 422 diffset.files.append(filediff)
419 423 diffset.changed_files += 1
420 424 if not patch['stats']['binary']:
421 425 diffset.lines_added += patch['stats']['added']
422 426 diffset.lines_deleted += patch['stats']['deleted']
423 427
424 428 return diffset
425 429
426 430 _lexer_cache = {}
427 431
428 432 def _get_lexer_for_filename(self, filename, filenode=None):
429 433 # cached because we might need to call it twice for source/target
430 434 if filename not in self._lexer_cache:
431 435 if filenode:
432 436 lexer = filenode.lexer
433 437 extension = filenode.extension
434 438 else:
435 439 lexer = FileNode.get_lexer(filename=filename)
436 440 extension = filename.split('.')[-1]
437 441
438 442 lexer = get_custom_lexer(extension) or lexer
439 443 self._lexer_cache[filename] = lexer
440 444 return self._lexer_cache[filename]
441 445
442 446 def render_patch(self, patch):
443 447 log.debug('rendering diff for %r', patch['filename'])
444 448
445 449 source_filename = patch['original_filename']
446 450 target_filename = patch['filename']
447 451
448 452 source_lexer = plain_text_lexer
449 453 target_lexer = plain_text_lexer
450 454
451 455 if not patch['stats']['binary']:
452 456 node_hl_mode = self.HL_NONE if patch['chunks'] == [] else None
453 457 hl_mode = node_hl_mode or self.highlight_mode
454 458
455 459 if hl_mode == self.HL_REAL:
456 460 if (source_filename and patch['operation'] in ('D', 'M')
457 461 and source_filename not in self.source_nodes):
458 462 self.source_nodes[source_filename] = (
459 463 self.source_node_getter(source_filename))
460 464
461 465 if (target_filename and patch['operation'] in ('A', 'M')
462 466 and target_filename not in self.target_nodes):
463 467 self.target_nodes[target_filename] = (
464 468 self.target_node_getter(target_filename))
465 469
466 470 elif hl_mode == self.HL_FAST:
467 471 source_lexer = self._get_lexer_for_filename(source_filename)
468 472 target_lexer = self._get_lexer_for_filename(target_filename)
469 473
470 474 source_file = self.source_nodes.get(source_filename, source_filename)
471 475 target_file = self.target_nodes.get(target_filename, target_filename)
472 476 raw_id_uid = ''
473 477 if self.source_nodes.get(source_filename):
474 478 raw_id_uid = self.source_nodes[source_filename].commit.raw_id
475 479
476 480 if not raw_id_uid and self.target_nodes.get(target_filename):
477 481 # in case this is a new file we only have it in target
478 482 raw_id_uid = self.target_nodes[target_filename].commit.raw_id
479 483
480 484 source_filenode, target_filenode = None, None
481 485
482 486 # TODO: dan: FileNode.lexer works on the content of the file - which
483 487 # can be slow - issue #4289 explains a lexer clean up - which once
484 488 # done can allow caching a lexer for a filenode to avoid the file lookup
485 489 if isinstance(source_file, FileNode):
486 490 source_filenode = source_file
487 491 #source_lexer = source_file.lexer
488 492 source_lexer = self._get_lexer_for_filename(source_filename)
489 493 source_file.lexer = source_lexer
490 494
491 495 if isinstance(target_file, FileNode):
492 496 target_filenode = target_file
493 497 #target_lexer = target_file.lexer
494 498 target_lexer = self._get_lexer_for_filename(target_filename)
495 499 target_file.lexer = target_lexer
496 500
497 501 source_file_path, target_file_path = None, None
498 502
499 503 if source_filename != '/dev/null':
500 504 source_file_path = source_filename
501 505 if target_filename != '/dev/null':
502 506 target_file_path = target_filename
503 507
504 508 source_file_type = source_lexer.name
505 509 target_file_type = target_lexer.name
506 510
507 511 filediff = AttributeDict({
508 512 'source_file_path': source_file_path,
509 513 'target_file_path': target_file_path,
510 514 'source_filenode': source_filenode,
511 515 'target_filenode': target_filenode,
512 516 'source_file_type': target_file_type,
513 517 'target_file_type': source_file_type,
514 518 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
515 519 'operation': patch['operation'],
516 520 'source_mode': patch['stats']['old_mode'],
517 521 'target_mode': patch['stats']['new_mode'],
518 522 'limited_diff': isinstance(patch, LimitedDiffContainer),
519 523 'hunks': [],
520 524 'hunk_ops': None,
521 525 'diffset': self,
522 526 'raw_id': raw_id_uid,
523 527 })
524 528
525 529 file_chunks = patch['chunks'][1:]
526 530 for hunk in file_chunks:
527 531 hunkbit = self.parse_hunk(hunk, source_file, target_file)
528 532 hunkbit.source_file_path = source_file_path
529 533 hunkbit.target_file_path = target_file_path
530 534 filediff.hunks.append(hunkbit)
531 535
532 536 # Simulate hunk on OPS type line which doesn't really contain any diff
533 537 # this allows commenting on those
534 538 if not file_chunks:
535 539 actions = []
536 540 for op_id, op_text in filediff.patch['stats']['ops'].items():
537 541 if op_id == DEL_FILENODE:
538 542 actions.append(u'file was removed')
539 543 elif op_id == BIN_FILENODE:
540 544 actions.append(u'binary diff hidden')
541 545 else:
542 546 actions.append(safe_unicode(op_text))
543 547 action_line = u'NO CONTENT: ' + \
544 548 u', '.join(actions) or u'UNDEFINED_ACTION'
545 549
546 550 hunk_ops = {'source_length': 0, 'source_start': 0,
547 551 'lines': [
548 552 {'new_lineno': 0, 'old_lineno': 1,
549 553 'action': 'unmod-no-hl', 'line': action_line}
550 554 ],
551 555 'section_header': u'', 'target_start': 1, 'target_length': 1}
552 556
553 557 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
554 558 hunkbit.source_file_path = source_file_path
555 559 hunkbit.target_file_path = target_file_path
556 560 filediff.hunk_ops = hunkbit
557 561 return filediff
558 562
559 563 def parse_hunk(self, hunk, source_file, target_file):
560 564 result = AttributeDict(dict(
561 565 source_start=hunk['source_start'],
562 566 source_length=hunk['source_length'],
563 567 target_start=hunk['target_start'],
564 568 target_length=hunk['target_length'],
565 569 section_header=hunk['section_header'],
566 570 lines=[],
567 571 ))
568 572 before, after = [], []
569 573
570 574 for line in hunk['lines']:
571 575 if line['action'] in ['unmod', 'unmod-no-hl']:
572 576 no_hl = line['action'] == 'unmod-no-hl'
573 577 result.lines.extend(
574 578 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
575 579 after.append(line)
576 580 before.append(line)
577 581 elif line['action'] == 'add':
578 582 after.append(line)
579 583 elif line['action'] == 'del':
580 584 before.append(line)
581 585 elif line['action'] == 'old-no-nl':
582 586 before.append(line)
583 587 elif line['action'] == 'new-no-nl':
584 588 after.append(line)
585 589
586 590 all_actions = [x['action'] for x in after] + [x['action'] for x in before]
587 591 no_hl = {x for x in all_actions} == {'unmod-no-hl'}
588 592 result.lines.extend(
589 593 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
590 594 # NOTE(marcink): we must keep list() call here so we can cache the result...
591 595 result.unified = list(self.as_unified(result.lines))
592 596 result.sideside = result.lines
593 597
594 598 return result
595 599
596 600 def parse_lines(self, before_lines, after_lines, source_file, target_file,
597 601 no_hl=False):
598 602 # TODO: dan: investigate doing the diff comparison and fast highlighting
599 603 # on the entire before and after buffered block lines rather than by
600 604 # line, this means we can get better 'fast' highlighting if the context
601 605 # allows it - eg.
602 606 # line 4: """
603 607 # line 5: this gets highlighted as a string
604 608 # line 6: """
605 609
606 610 lines = []
607 611
608 612 before_newline = AttributeDict()
609 613 after_newline = AttributeDict()
610 614 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
611 615 before_newline_line = before_lines.pop(-1)
612 616 before_newline.content = '\n {}'.format(
613 617 render_tokenstream(
614 618 [(x[0], '', x[1])
615 619 for x in [('nonl', before_newline_line['line'])]]))
616 620
617 621 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
618 622 after_newline_line = after_lines.pop(-1)
619 623 after_newline.content = '\n {}'.format(
620 624 render_tokenstream(
621 625 [(x[0], '', x[1])
622 626 for x in [('nonl', after_newline_line['line'])]]))
623 627
624 628 while before_lines or after_lines:
625 629 before, after = None, None
626 630 before_tokens, after_tokens = None, None
627 631
628 632 if before_lines:
629 633 before = before_lines.pop(0)
630 634 if after_lines:
631 635 after = after_lines.pop(0)
632 636
633 637 original = AttributeDict()
634 638 modified = AttributeDict()
635 639
636 640 if before:
637 641 if before['action'] == 'old-no-nl':
638 642 before_tokens = [('nonl', before['line'])]
639 643 else:
640 644 before_tokens = self.get_line_tokens(
641 645 line_text=before['line'], line_number=before['old_lineno'],
642 646 input_file=source_file, no_hl=no_hl)
643 647 original.lineno = before['old_lineno']
644 648 original.content = before['line']
645 649 original.action = self.action_to_op(before['action'])
646 650
647 651 original.get_comment_args = (
648 652 source_file, 'o', before['old_lineno'])
649 653
650 654 if after:
651 655 if after['action'] == 'new-no-nl':
652 656 after_tokens = [('nonl', after['line'])]
653 657 else:
654 658 after_tokens = self.get_line_tokens(
655 659 line_text=after['line'], line_number=after['new_lineno'],
656 660 input_file=target_file, no_hl=no_hl)
657 661 modified.lineno = after['new_lineno']
658 662 modified.content = after['line']
659 663 modified.action = self.action_to_op(after['action'])
660 664
661 665 modified.get_comment_args = (target_file, 'n', after['new_lineno'])
662 666
663 667 # diff the lines
664 668 if before_tokens and after_tokens:
665 669 o_tokens, m_tokens, similarity = tokens_diff(
666 670 before_tokens, after_tokens)
667 671 original.content = render_tokenstream(o_tokens)
668 672 modified.content = render_tokenstream(m_tokens)
669 673 elif before_tokens:
670 674 original.content = render_tokenstream(
671 675 [(x[0], '', x[1]) for x in before_tokens])
672 676 elif after_tokens:
673 677 modified.content = render_tokenstream(
674 678 [(x[0], '', x[1]) for x in after_tokens])
675 679
676 680 if not before_lines and before_newline:
677 681 original.content += before_newline.content
678 682 before_newline = None
679 683 if not after_lines and after_newline:
680 684 modified.content += after_newline.content
681 685 after_newline = None
682 686
683 687 lines.append(AttributeDict({
684 688 'original': original,
685 689 'modified': modified,
686 690 }))
687 691
688 692 return lines
689 693
690 694 def get_line_tokens(self, line_text, line_number, input_file=None, no_hl=False):
691 695 filenode = None
692 696 filename = None
693 697
694 698 if isinstance(input_file, basestring):
695 699 filename = input_file
696 700 elif isinstance(input_file, FileNode):
697 701 filenode = input_file
698 702 filename = input_file.unicode_path
699 703
700 704 hl_mode = self.HL_NONE if no_hl else self.highlight_mode
701 705 if hl_mode == self.HL_REAL and filenode:
702 706 lexer = self._get_lexer_for_filename(filename)
703 707 file_size_allowed = input_file.size < self.max_file_size_limit
704 708 if line_number and file_size_allowed:
705 709 return self.get_tokenized_filenode_line(
706 710 input_file, line_number, lexer)
707 711
708 712 if hl_mode in (self.HL_REAL, self.HL_FAST) and filename:
709 713 lexer = self._get_lexer_for_filename(filename)
710 714 return list(tokenize_string(line_text, lexer))
711 715
712 716 return list(tokenize_string(line_text, plain_text_lexer))
713 717
714 718 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
715 719
716 720 if filenode not in self.highlighted_filenodes:
717 721 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
718 722 self.highlighted_filenodes[filenode] = tokenized_lines
719 723 return self.highlighted_filenodes[filenode][line_number - 1]
720 724
721 725 def action_to_op(self, action):
722 726 return {
723 727 'add': '+',
724 728 'del': '-',
725 729 'unmod': ' ',
726 730 'unmod-no-hl': ' ',
727 731 'old-no-nl': ' ',
728 732 'new-no-nl': ' ',
729 733 }.get(action, action)
730 734
731 735 def as_unified(self, lines):
732 736 """
733 737 Return a generator that yields the lines of a diff in unified order
734 738 """
735 739 def generator():
736 740 buf = []
737 741 for line in lines:
738 742
739 743 if buf and not line.original or line.original.action == ' ':
740 744 for b in buf:
741 745 yield b
742 746 buf = []
743 747
744 748 if line.original:
745 749 if line.original.action == ' ':
746 750 yield (line.original.lineno, line.modified.lineno,
747 751 line.original.action, line.original.content,
748 752 line.original.get_comment_args)
749 753 continue
750 754
751 755 if line.original.action == '-':
752 756 yield (line.original.lineno, None,
753 757 line.original.action, line.original.content,
754 758 line.original.get_comment_args)
755 759
756 760 if line.modified.action == '+':
757 761 buf.append((
758 762 None, line.modified.lineno,
759 763 line.modified.action, line.modified.content,
760 764 line.modified.get_comment_args))
761 765 continue
762 766
763 767 if line.modified:
764 768 yield (None, line.modified.lineno,
765 769 line.modified.action, line.modified.content,
766 770 line.modified.get_comment_args)
767 771
768 772 for b in buf:
769 773 yield b
770 774
771 775 return generator()
@@ -1,1016 +1,1022 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26
27 27
28 28 <%def name="render_diffset(diffset, commit=None,
29 29
30 30 # collapse all file diff entries when there are more than this amount of files in the diff
31 31 collapse_when_files_over=20,
32 32
33 33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 34 lines_changed_limit=500,
35 35
36 36 # add a ruler at to the output
37 37 ruler_at_chars=0,
38 38
39 39 # show inline comments
40 40 use_comments=False,
41 41
42 42 # disable new comments
43 43 disable_new_comments=False,
44 44
45 45 # special file-comments that were deleted in previous versions
46 46 # it's used for showing outdated comments for deleted files in a PR
47 47 deleted_files_comments=None,
48 48
49 49 # for cache purpose
50 50 inline_comments=None,
51 51
52 52 )">
53 53 %if use_comments:
54 54 <div id="cb-comments-inline-container-template" class="js-template">
55 55 ${inline_comments_container([], inline_comments)}
56 56 </div>
57 57 <div class="js-template" id="cb-comment-inline-form-template">
58 58 <div class="comment-inline-form ac">
59 59
60 60 %if c.rhodecode_user.username != h.DEFAULT_USER:
61 61 ## render template for inline comments
62 62 ${commentblock.comment_form(form_type='inline')}
63 63 %else:
64 64 ${h.form('', class_='inline-form comment-form-login', method='get')}
65 65 <div class="pull-left">
66 66 <div class="comment-help pull-right">
67 67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
68 68 </div>
69 69 </div>
70 70 <div class="comment-button pull-right">
71 71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
72 72 ${_('Cancel')}
73 73 </button>
74 74 </div>
75 75 <div class="clearfix"></div>
76 76 ${h.end_form()}
77 77 %endif
78 78 </div>
79 79 </div>
80 80
81 81 %endif
82 82 <%
83 83 collapse_all = len(diffset.files) > collapse_when_files_over
84 84 %>
85 85
86 86 %if c.user_session_attrs["diffmode"] == 'sideside':
87 87 <style>
88 88 .wrapper {
89 89 max-width: 1600px !important;
90 90 }
91 91 </style>
92 92 %endif
93 93
94 94 %if ruler_at_chars:
95 95 <style>
96 96 .diff table.cb .cb-content:after {
97 97 content: "";
98 98 border-left: 1px solid blue;
99 99 position: absolute;
100 100 top: 0;
101 101 height: 18px;
102 102 opacity: .2;
103 103 z-index: 10;
104 104 //## +5 to account for diff action (+/-)
105 105 left: ${ruler_at_chars + 5}ch;
106 106 </style>
107 107 %endif
108 108
109 109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
111 111 %if commit:
112 112 <div class="pull-right">
113 113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
114 114 ${_('Browse Files')}
115 115 </a>
116 116 </div>
117 117 %endif
118 118 <h2 class="clearinner">
119 119 ## invidual commit
120 120 % if commit:
121 121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
122 122 ${h.age_component(commit.date)}
123 123 % if diffset.limited_diff:
124 124 - ${_('The requested commit is too big and content was truncated.')}
125 125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 126 <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>
127 127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
128 128 ## compare diff, has no file-selector and we want to show stats anyway
129 129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
130 130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
131 131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
132 132 % endif
133 133 % else:
134 134 ## pull requests/compare
135 135 ${_('File Changes')}
136 136 % endif
137 137
138 138 </h2>
139 139 </div>
140 140
141 141 %if diffset.has_hidden_changes:
142 142 <p class="empty_data">${_('Some changes may be hidden')}</p>
143 143 %elif not diffset.files:
144 144 <p class="empty_data">${_('No files')}</p>
145 145 %endif
146 146
147 147 <div class="filediffs">
148 148
149 149 ## initial value could be marked as False later on
150 150 <% over_lines_changed_limit = False %>
151 151 %for i, filediff in enumerate(diffset.files):
152 152
153 153 <%
154 154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
155 155 over_lines_changed_limit = lines_changed > lines_changed_limit
156 156 %>
157 157 ## anchor with support of sticky header
158 158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
159 159
160 160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
161 161 <div
162 162 class="filediff"
163 163 data-f-path="${filediff.patch['filename']}"
164 164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
165 165 >
166 166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
167 167 <div class="filediff-collapse-indicator"></div>
168 168 ${diff_ops(filediff)}
169 169 </label>
170 170
171 171 ${diff_menu(filediff, use_comments=use_comments)}
172 172 <table 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 '')}">
173 173
174 174 ## new/deleted/empty content case
175 175 % if not filediff.hunks:
176 176 ## Comment container, on "fakes" hunk that contains all data to render comments
177 177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
178 178 % endif
179 179
180 180 %if filediff.limited_diff:
181 181 <tr class="cb-warning cb-collapser">
182 182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
183 183 ${_('The requested commit 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>
184 184 </td>
185 185 </tr>
186 186 %else:
187 187 %if over_lines_changed_limit:
188 188 <tr class="cb-warning cb-collapser">
189 189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
190 190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
191 191 <a href="#" class="cb-expand"
192 192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
193 193 </a>
194 194 <a href="#" class="cb-collapse"
195 195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
196 196 </a>
197 197 </td>
198 198 </tr>
199 199 %endif
200 200 %endif
201 201
202 202 % for hunk in filediff.hunks:
203 203 <tr class="cb-hunk">
204 204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
205 205 ## TODO: dan: add ajax loading of more context here
206 206 ## <a href="#">
207 207 <i class="icon-more"></i>
208 208 ## </a>
209 209 </td>
210 210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
211 211 @@
212 212 -${hunk.source_start},${hunk.source_length}
213 213 +${hunk.target_start},${hunk.target_length}
214 214 ${hunk.section_header}
215 215 </td>
216 216 </tr>
217 217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
218 218 % endfor
219 219
220 220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
221 221
222 222 ## outdated comments that do not fit into currently displayed lines
223 223 % for lineno, comments in unmatched_comments.items():
224 224
225 225 %if c.user_session_attrs["diffmode"] == 'unified':
226 226 % if loop.index == 0:
227 227 <tr class="cb-hunk">
228 228 <td colspan="3"></td>
229 229 <td>
230 230 <div>
231 231 ${_('Unmatched inline comments below')}
232 232 </div>
233 233 </td>
234 234 </tr>
235 235 % endif
236 236 <tr class="cb-line">
237 237 <td class="cb-data cb-context"></td>
238 238 <td class="cb-lineno cb-context"></td>
239 239 <td class="cb-lineno cb-context"></td>
240 240 <td class="cb-content cb-context">
241 241 ${inline_comments_container(comments, inline_comments)}
242 242 </td>
243 243 </tr>
244 244 %elif c.user_session_attrs["diffmode"] == 'sideside':
245 245 % if loop.index == 0:
246 246 <tr class="cb-comment-info">
247 247 <td colspan="2"></td>
248 248 <td class="cb-line">
249 249 <div>
250 250 ${_('Unmatched inline comments below')}
251 251 </div>
252 252 </td>
253 253 <td colspan="2"></td>
254 254 <td class="cb-line">
255 255 <div>
256 256 ${_('Unmatched comments below')}
257 257 </div>
258 258 </td>
259 259 </tr>
260 260 % endif
261 261 <tr class="cb-line">
262 262 <td class="cb-data cb-context"></td>
263 263 <td class="cb-lineno cb-context"></td>
264 264 <td class="cb-content cb-context">
265 265 % if lineno.startswith('o'):
266 266 ${inline_comments_container(comments, inline_comments)}
267 267 % endif
268 268 </td>
269 269
270 270 <td class="cb-data cb-context"></td>
271 271 <td class="cb-lineno cb-context"></td>
272 272 <td class="cb-content cb-context">
273 273 % if lineno.startswith('n'):
274 274 ${inline_comments_container(comments, inline_comments)}
275 275 % endif
276 276 </td>
277 277 </tr>
278 278 %endif
279 279
280 280 % endfor
281 281
282 282 </table>
283 283 </div>
284 284 %endfor
285 285
286 286 ## outdated comments that are made for a file that has been deleted
287 287 % for filename, comments_dict in (deleted_files_comments or {}).items():
288 288 <%
289 289 display_state = 'display: none'
290 290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
291 291 if open_comments_in_file:
292 292 display_state = ''
293 293 %>
294 294 <div class="filediffs filediff-outdated" style="${display_state}">
295 295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
296 296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
297 297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
298 298 <div class="filediff-collapse-indicator"></div>
299 299 <span class="pill">
300 300 ## file was deleted
301 301 <strong>${filename}</strong>
302 302 </span>
303 303 <span class="pill-group" style="float: left">
304 304 ## file op, doesn't need translation
305 305 <span class="pill" op="removed">removed in this version</span>
306 306 </span>
307 307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
308 308 <span class="pill-group" style="float: right">
309 309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
310 310 </span>
311 311 </label>
312 312
313 313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
314 314 <tr>
315 315 % if c.user_session_attrs["diffmode"] == 'unified':
316 316 <td></td>
317 317 %endif
318 318
319 319 <td></td>
320 320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
321 321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
322 322 </td>
323 323 </tr>
324 324 %if c.user_session_attrs["diffmode"] == 'unified':
325 325 <tr class="cb-line">
326 326 <td class="cb-data cb-context"></td>
327 327 <td class="cb-lineno cb-context"></td>
328 328 <td class="cb-lineno cb-context"></td>
329 329 <td class="cb-content cb-context">
330 330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
331 331 </td>
332 332 </tr>
333 333 %elif c.user_session_attrs["diffmode"] == 'sideside':
334 334 <tr class="cb-line">
335 335 <td class="cb-data cb-context"></td>
336 336 <td class="cb-lineno cb-context"></td>
337 337 <td class="cb-content cb-context"></td>
338 338
339 339 <td class="cb-data cb-context"></td>
340 340 <td class="cb-lineno cb-context"></td>
341 341 <td class="cb-content cb-context">
342 342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
343 343 </td>
344 344 </tr>
345 345 %endif
346 346 </table>
347 347 </div>
348 348 </div>
349 349 % endfor
350 350
351 351 </div>
352 352 </div>
353 353 </%def>
354 354
355 355 <%def name="diff_ops(filediff)">
356 356 <%
357 357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
358 358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
359 359 %>
360 360 <span class="pill">
361 361 %if filediff.source_file_path and filediff.target_file_path:
362 362 %if filediff.source_file_path != filediff.target_file_path:
363 363 ## file was renamed, or copied
364 364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
365 365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
366 366 <% final_path = filediff.target_file_path %>
367 367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
368 368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
369 369 <% final_path = filediff.target_file_path %>
370 370 %endif
371 371 %else:
372 372 ## file was modified
373 373 <strong>${filediff.source_file_path}</strong>
374 374 <% final_path = filediff.source_file_path %>
375 375 %endif
376 376 %else:
377 377 %if filediff.source_file_path:
378 378 ## file was deleted
379 379 <strong>${filediff.source_file_path}</strong>
380 380 <% final_path = filediff.source_file_path %>
381 381 %else:
382 382 ## file was added
383 383 <strong>${filediff.target_file_path}</strong>
384 384 <% final_path = filediff.target_file_path %>
385 385 %endif
386 386 %endif
387 387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
388 388 </span>
389 389 ## anchor link
390 390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
391 391
392 392 <span class="pill-group" style="float: right">
393 393
394 394 ## ops pills
395 395 %if filediff.limited_diff:
396 396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
397 397 %endif
398 398
399 399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
400 400 <span class="pill" op="created">created</span>
401 401 %if filediff['target_mode'].startswith('120'):
402 402 <span class="pill" op="symlink">symlink</span>
403 403 %else:
404 404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
405 405 %endif
406 406 %endif
407 407
408 408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
409 409 <span class="pill" op="renamed">renamed</span>
410 410 %endif
411 411
412 412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
413 413 <span class="pill" op="copied">copied</span>
414 414 %endif
415 415
416 416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
417 417 <span class="pill" op="removed">removed</span>
418 418 %endif
419 419
420 420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
421 421 <span class="pill" op="mode">
422 422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
423 423 </span>
424 424 %endif
425 425
426 426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
427 427 <span class="pill" op="binary">binary</span>
428 428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
429 429 <span class="pill" op="modified">modified</span>
430 430 %endif
431 431 %endif
432 432
433 433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
434 434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
435 435
436 436 </span>
437 437
438 438 </%def>
439 439
440 440 <%def name="nice_mode(filemode)">
441 441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
442 442 </%def>
443 443
444 444 <%def name="diff_menu(filediff, use_comments=False)">
445 445 <div class="filediff-menu">
446 %if filediff.diffset.source_ref:
446
447 %if filediff.diffset.source_ref:
448
449 ## FILE BEFORE CHANGES
447 450 %if filediff.operation in ['D', 'M']:
448 451 <a
449 452 class="tooltip"
450 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
453 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)}"
451 454 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
452 455 >
453 456 ${_('Show file before')}
454 457 </a> |
455 458 %else:
456 459 <span
457 460 class="tooltip"
458 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
459 462 >
460 463 ${_('Show file before')}
461 464 </span> |
462 465 %endif
466
467 ## FILE AFTER CHANGES
463 468 %if filediff.operation in ['A', 'M']:
464 469 <a
465 470 class="tooltip"
466 471 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)}"
467 472 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
468 473 >
469 474 ${_('Show file after')}
470 475 </a>
471 476 %else:
472 477 <span
473 478 class="tooltip"
474 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
479 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
475 480 >
476 481 ${_('Show file after')}
477 482 </span>
478 483 %endif
479 484
480 % if use_comments:
481 |
482 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
483 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
484 </a>
485 % endif
485 % if use_comments:
486 |
487 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
488 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
489 </a>
490 % endif
486 491
487 %endif
492 %endif
493
488 494 </div>
489 495 </%def>
490 496
491 497
492 498 <%def name="inline_comments_container(comments, inline_comments)">
493 499 <div class="inline-comments">
494 500 %for comment in comments:
495 501 ${commentblock.comment_block(comment, inline=True)}
496 502 %endfor
497 503 % if comments and comments[-1].outdated:
498 504 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
499 505 style="display: none;}">
500 506 ${_('Add another comment')}
501 507 </span>
502 508 % else:
503 509 <span onclick="return Rhodecode.comments.createComment(this)"
504 510 class="btn btn-secondary cb-comment-add-button">
505 511 ${_('Add another comment')}
506 512 </span>
507 513 % endif
508 514
509 515 </div>
510 516 </%def>
511 517
512 518 <%!
513 519 def get_comments_for(diff_type, comments, filename, line_version, line_number):
514 520 if hasattr(filename, 'unicode_path'):
515 521 filename = filename.unicode_path
516 522
517 523 if not isinstance(filename, basestring):
518 524 return None
519 525
520 526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
521 527
522 528 if comments and filename in comments:
523 529 file_comments = comments[filename]
524 530 if line_key in file_comments:
525 531 data = file_comments.pop(line_key)
526 532 return data
527 533 %>
528 534
529 535 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
530 536 %for i, line in enumerate(hunk.sideside):
531 537 <%
532 538 old_line_anchor, new_line_anchor = None, None
533 539
534 540 if line.original.lineno:
535 541 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
536 542 if line.modified.lineno:
537 543 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
538 544 %>
539 545
540 546 <tr class="cb-line">
541 547 <td class="cb-data ${action_class(line.original.action)}"
542 548 data-line-no="${line.original.lineno}"
543 549 >
544 550 <div>
545 551
546 552 <% line_old_comments = None %>
547 553 %if line.original.get_comment_args:
548 554 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
549 555 %endif
550 556 %if line_old_comments:
551 557 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
552 558 % if has_outdated:
553 559 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
554 560 % else:
555 561 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
556 562 % endif
557 563 %endif
558 564 </div>
559 565 </td>
560 566 <td class="cb-lineno ${action_class(line.original.action)}"
561 567 data-line-no="${line.original.lineno}"
562 568 %if old_line_anchor:
563 569 id="${old_line_anchor}"
564 570 %endif
565 571 >
566 572 %if line.original.lineno:
567 573 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
568 574 %endif
569 575 </td>
570 576 <td class="cb-content ${action_class(line.original.action)}"
571 577 data-line-no="o${line.original.lineno}"
572 578 >
573 579 %if use_comments and line.original.lineno:
574 580 ${render_add_comment_button()}
575 581 %endif
576 582 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
577 583
578 584 %if use_comments and line.original.lineno and line_old_comments:
579 585 ${inline_comments_container(line_old_comments, inline_comments)}
580 586 %endif
581 587
582 588 </td>
583 589 <td class="cb-data ${action_class(line.modified.action)}"
584 590 data-line-no="${line.modified.lineno}"
585 591 >
586 592 <div>
587 593
588 594 %if line.modified.get_comment_args:
589 595 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
590 596 %else:
591 597 <% line_new_comments = None%>
592 598 %endif
593 599 %if line_new_comments:
594 600 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
595 601 % if has_outdated:
596 602 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
597 603 % else:
598 604 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
599 605 % endif
600 606 %endif
601 607 </div>
602 608 </td>
603 609 <td class="cb-lineno ${action_class(line.modified.action)}"
604 610 data-line-no="${line.modified.lineno}"
605 611 %if new_line_anchor:
606 612 id="${new_line_anchor}"
607 613 %endif
608 614 >
609 615 %if line.modified.lineno:
610 616 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
611 617 %endif
612 618 </td>
613 619 <td class="cb-content ${action_class(line.modified.action)}"
614 620 data-line-no="n${line.modified.lineno}"
615 621 >
616 622 %if use_comments and line.modified.lineno:
617 623 ${render_add_comment_button()}
618 624 %endif
619 625 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
620 626 %if use_comments and line.modified.lineno and line_new_comments:
621 627 ${inline_comments_container(line_new_comments, inline_comments)}
622 628 %endif
623 629 </td>
624 630 </tr>
625 631 %endfor
626 632 </%def>
627 633
628 634
629 635 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
630 636 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
631 637
632 638 <%
633 639 old_line_anchor, new_line_anchor = None, None
634 640 if old_line_no:
635 641 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
636 642 if new_line_no:
637 643 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
638 644 %>
639 645 <tr class="cb-line">
640 646 <td class="cb-data ${action_class(action)}">
641 647 <div>
642 648
643 649 %if comments_args:
644 650 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
645 651 %else:
646 652 <% comments = None %>
647 653 %endif
648 654
649 655 % if comments:
650 656 <% has_outdated = any([x.outdated for x in comments]) %>
651 657 % if has_outdated:
652 658 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
653 659 % else:
654 660 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
655 661 % endif
656 662 % endif
657 663 </div>
658 664 </td>
659 665 <td class="cb-lineno ${action_class(action)}"
660 666 data-line-no="${old_line_no}"
661 667 %if old_line_anchor:
662 668 id="${old_line_anchor}"
663 669 %endif
664 670 >
665 671 %if old_line_anchor:
666 672 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
667 673 %endif
668 674 </td>
669 675 <td class="cb-lineno ${action_class(action)}"
670 676 data-line-no="${new_line_no}"
671 677 %if new_line_anchor:
672 678 id="${new_line_anchor}"
673 679 %endif
674 680 >
675 681 %if new_line_anchor:
676 682 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
677 683 %endif
678 684 </td>
679 685 <td class="cb-content ${action_class(action)}"
680 686 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
681 687 >
682 688 %if use_comments:
683 689 ${render_add_comment_button()}
684 690 %endif
685 691 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
686 692 %if use_comments and comments:
687 693 ${inline_comments_container(comments, inline_comments)}
688 694 %endif
689 695 </td>
690 696 </tr>
691 697 %endfor
692 698 </%def>
693 699
694 700
695 701 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
696 702 % if diff_mode == 'unified':
697 703 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
698 704 % elif diff_mode == 'sideside':
699 705 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
700 706 % else:
701 707 <tr class="cb-line">
702 708 <td>unknown diff mode</td>
703 709 </tr>
704 710 % endif
705 711 </%def>file changes
706 712
707 713
708 714 <%def name="render_add_comment_button()">
709 715 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
710 716 <span><i class="icon-comment"></i></span>
711 717 </button>
712 718 </%def>
713 719
714 720 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
715 721
716 722 <div id="diff-file-sticky" class="diffset-menu clearinner">
717 723 ## auto adjustable
718 724 <div class="sidebar__inner">
719 725 <div class="sidebar__bar">
720 726 <div class="pull-right">
721 727 <div class="btn-group">
722 728
723 729 ## DIFF OPTIONS via Select2
724 730 <div class="pull-left">
725 731 ${h.hidden('diff_menu')}
726 732 </div>
727 733
728 734 <a
729 735 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
730 736 title="${h.tooltip(_('View side by side'))}"
731 737 href="${h.current_route_path(request, diffmode='sideside')}">
732 738 <span>${_('Side by Side')}</span>
733 739 </a>
734 740
735 741 <a
736 742 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
737 743 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
738 744 <span>${_('Unified')}</span>
739 745 </a>
740 746
741 747 % if range_diff_on is True:
742 748 <a
743 749 title="${_('Turn off: Show the diff as commit range')}"
744 750 class="btn btn-primary"
745 751 href="${h.current_route_path(request, **{"range-diff":"0"})}">
746 752 <span>${_('Range Diff')}</span>
747 753 </a>
748 754 % elif range_diff_on is False:
749 755 <a
750 756 title="${_('Show the diff as commit range')}"
751 757 class="btn"
752 758 href="${h.current_route_path(request, **{"range-diff":"1"})}">
753 759 <span>${_('Range Diff')}</span>
754 760 </a>
755 761 % endif
756 762 </div>
757 763 </div>
758 764 <div class="pull-left">
759 765 <div class="btn-group">
760 766 <div class="pull-left">
761 767 ${h.hidden('file_filter')}
762 768 </div>
763 769 <a
764 770 class="btn"
765 771 href="#"
766 772 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
767 773 <a
768 774 class="btn"
769 775 href="#"
770 776 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
771 777 </div>
772 778 </div>
773 779 </div>
774 780 <div class="fpath-placeholder">
775 781 <i class="icon-file-text"></i>
776 782 <strong class="fpath-placeholder-text">
777 783 Context file:
778 784 </strong>
779 785 </div>
780 786 <div class="sidebar_inner_shadow"></div>
781 787 </div>
782 788 </div>
783 789
784 790 % if diffset:
785 791
786 792 %if diffset.limited_diff:
787 793 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
788 794 %else:
789 795 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
790 796 %endif
791 797 ## case on range-diff placeholder needs to be updated
792 798 % if range_diff_on is True:
793 799 <% file_placeholder = _('Disabled on range diff') %>
794 800 % endif
795 801
796 802 <script>
797 803
798 804 var feedFilesOptions = function (query, initialData) {
799 805 var data = {results: []};
800 806 var isQuery = typeof query.term !== 'undefined';
801 807
802 808 var section = _gettext('Changed files');
803 809 var filteredData = [];
804 810
805 811 //filter results
806 812 $.each(initialData.results, function (idx, value) {
807 813
808 814 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
809 815 filteredData.push({
810 816 'id': this.id,
811 817 'text': this.text,
812 818 "ops": this.ops,
813 819 })
814 820 }
815 821
816 822 });
817 823
818 824 data.results = filteredData;
819 825
820 826 query.callback(data);
821 827 };
822 828
823 829 var formatFileResult = function(result, container, query, escapeMarkup) {
824 830 return function(data, escapeMarkup) {
825 831 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
826 832 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
827 833 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
828 834 '<span class="pill" op="added">{0}</span>' +
829 835 '<span class="pill" op="deleted">{1}</span>' +
830 836 '</span>'
831 837 ;
832 838 var added = data['ops']['added'];
833 839 if (added === 0) {
834 840 // don't show +0
835 841 added = 0;
836 842 } else {
837 843 added = '+' + added;
838 844 }
839 845
840 846 var deleted = -1*data['ops']['deleted'];
841 847
842 848 tmpl += pill.format(added, deleted);
843 849 return container.format(tmpl);
844 850
845 851 }(result, escapeMarkup);
846 852 };
847 853 var preloadData = {
848 854 results: [
849 855 % for filediff in diffset.files:
850 856 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
851 857 text:"${filediff.patch['filename']}",
852 858 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
853 859 % endfor
854 860 ]
855 861 };
856 862
857 863 $(document).ready(function () {
858 864
859 865 var fileFilter = $("#file_filter").select2({
860 866 'dropdownAutoWidth': true,
861 867 'width': 'auto',
862 868 'placeholder': "${file_placeholder}",
863 869 containerCssClass: "drop-menu",
864 870 dropdownCssClass: "drop-menu-dropdown",
865 871 data: preloadData,
866 872 query: function(query) {
867 873 feedFilesOptions(query, preloadData);
868 874 },
869 875 formatResult: formatFileResult
870 876 });
871 877 % if range_diff_on is True:
872 878 fileFilter.select2("enable", false);
873 879
874 880 % endif
875 881
876 882 $("#file_filter").on('click', function (e) {
877 883 e.preventDefault();
878 884 var selected = $('#file_filter').select2('data');
879 885 var idSelector = "#"+selected.id;
880 886 window.location.hash = idSelector;
881 887 // expand the container if we quick-select the field
882 888 $(idSelector).next().prop('checked', false);
883 889 updateSticky()
884 890 });
885 891
886 892 var contextPrefix = _gettext('Context file: ');
887 893 ## sticky sidebar
888 894 var sidebarElement = document.getElementById('diff-file-sticky');
889 895 sidebar = new StickySidebar(sidebarElement, {
890 896 topSpacing: 0,
891 897 bottomSpacing: 0,
892 898 innerWrapperSelector: '.sidebar__inner'
893 899 });
894 900 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
895 901 // reset our file so it's not holding new value
896 902 $('.fpath-placeholder-text').html(contextPrefix)
897 903 });
898 904
899 905 updateSticky = function () {
900 906 sidebar.updateSticky();
901 907 Waypoint.refreshAll();
902 908 };
903 909
904 910 var animateText = $.debounce(100, function(fPath, anchorId) {
905 911 // animate setting the text
906 912 var callback = function () {
907 913 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
908 914 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
909 915 };
910 916 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
911 917 });
912 918
913 919 ## dynamic file waypoints
914 920 var setFPathInfo = function(fPath, anchorId){
915 921 animateText(fPath, anchorId)
916 922 };
917 923
918 924 var codeBlock = $('.filediff');
919 925 // forward waypoint
920 926 codeBlock.waypoint(
921 927 function(direction) {
922 928 if (direction === "down"){
923 929 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
924 930 }
925 931 }, {
926 932 offset: 70,
927 933 context: '.fpath-placeholder'
928 934 }
929 935 );
930 936
931 937 // backward waypoint
932 938 codeBlock.waypoint(
933 939 function(direction) {
934 940 if (direction === "up"){
935 941 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
936 942 }
937 943 }, {
938 944 offset: function () {
939 945 return -this.element.clientHeight + 90
940 946 },
941 947 context: '.fpath-placeholder'
942 948 }
943 949 );
944 950
945 951 var preloadData = {
946 952 results: [
947 953 ## Wide diff mode
948 954 {
949 955 id: 1,
950 956 text: _gettext('Toggle Wide Mode diff'),
951 957 action: function () {
952 958 updateSticky();
953 959 Rhodecode.comments.toggleWideMode(this);
954 960 return null;
955 961 },
956 962 url: null,
957 963 },
958 964
959 965 ## Whitespace change
960 966 % if request.GET.get('ignorews', '') == '1':
961 967 {
962 968 id: 2,
963 969 text: _gettext('Show whitespace changes'),
964 970 action: function () {},
965 971 url: "${h.current_route_path(request, ignorews=0)|n}"
966 972 },
967 973 % else:
968 974 {
969 975 id: 2,
970 976 text: _gettext('Hide whitespace changes'),
971 977 action: function () {},
972 978 url: "${h.current_route_path(request, ignorews=1)|n}"
973 979 },
974 980 % endif
975 981
976 982 ## FULL CONTEXT
977 983 % if request.GET.get('fullcontext', '') == '1':
978 984 {
979 985 id: 3,
980 986 text: _gettext('Hide full context diff'),
981 987 action: function () {},
982 988 url: "${h.current_route_path(request, fullcontext=0)|n}"
983 989 },
984 990 % else:
985 991 {
986 992 id: 3,
987 993 text: _gettext('Show full context diff'),
988 994 action: function () {},
989 995 url: "${h.current_route_path(request, fullcontext=1)|n}"
990 996 },
991 997 % endif
992 998
993 999 ]
994 1000 };
995 1001
996 1002 $("#diff_menu").select2({
997 1003 minimumResultsForSearch: -1,
998 1004 containerCssClass: "drop-menu",
999 1005 dropdownCssClass: "drop-menu-dropdown",
1000 1006 dropdownAutoWidth: true,
1001 1007 data: preloadData,
1002 1008 placeholder: "${_('Diff Options')}",
1003 1009 });
1004 1010 $("#diff_menu").on('select2-selecting', function (e) {
1005 1011 e.choice.action();
1006 1012 if (e.choice.url !== null) {
1007 1013 window.location = e.choice.url
1008 1014 }
1009 1015 });
1010 1016
1011 1017 });
1012 1018
1013 1019 </script>
1014 1020 % endif
1015 1021
1016 1022 </%def>
General Comments 0
You need to be logged in to leave comments. Login now