##// END OF EJS Templates
diffs: exclude +/- from diffs generation and make then a non-selectable markers visible only via CSS.
dan -
r3135:5cc59605 default
parent child Browse files
Show More
@@ -1,318 +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 91 response.mustcontain('File no longer 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 136 response.mustcontain('File no longer 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 166 response.mustcontain('File no longer 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 def _check_diff_menus(self, response, right_menu=False,):
297 # diff menus
298 for elem in ['Show File', 'Unified Diff', 'Side-by-side Diff',
299 'Raw Diff', 'Download Diff']:
296 def _check_new_diff_menus(self, response, right_menu=False,):
297 # individual file diff menus
298 for elem in ['Show file before', 'Show file after']:
300 299 response.mustcontain(elem)
301 300
302 301 # right pane diff menus
303 302 if right_menu:
304 for elem in ['Ignore whitespace', 'Increase context',
305 'Hide comments']:
303 for elem in ['Hide whitespace changes', 'Toggle Wide Mode diff',
304 'Show full context diff']:
306 305 response.mustcontain(elem)
307
308 def _check_new_diff_menus(self, response, right_menu=False,):
309 # diff menus
310 for elem in ['Show file before', 'Show file after',
311 'Raw diff', 'Download diff']:
312 response.mustcontain(elem)
313
314 # right pane diff menus
315 if right_menu:
316 for elem in ['Ignore whitespace', 'Increase context',
317 'Hide comments']:
318 response.mustcontain(elem)
@@ -1,1237 +1,1237 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
22 22 """
23 23 Set of diffing helpers, previously part of vcs
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import bz2
29 29
30 30 import collections
31 31 import difflib
32 32 import logging
33 33 import cPickle as pickle
34 34 from itertools import tee, imap
35 35
36 36 from rhodecode.lib.vcs.exceptions import VCSError
37 37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42 # define max context, a file with more than this numbers of lines is unusable
43 43 # in browser anyway
44 44 MAX_CONTEXT = 20 * 1024
45 45 DEFAULT_CONTEXT = 3
46 46
47 47
48 48 def get_diff_context(request):
49 49 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
50 50
51 51
52 52 def get_diff_whitespace_flag(request):
53 53 return request.GET.get('ignorews', '') == '1'
54 54
55 55
56 56 class OPS(object):
57 57 ADD = 'A'
58 58 MOD = 'M'
59 59 DEL = 'D'
60 60
61 61
62 62 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
63 63 """
64 64 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
65 65
66 66 :param ignore_whitespace: ignore whitespaces in diff
67 67 """
68 68 # make sure we pass in default context
69 69 context = context or 3
70 70 # protect against IntOverflow when passing HUGE context
71 71 if context > MAX_CONTEXT:
72 72 context = MAX_CONTEXT
73 73
74 74 submodules = filter(lambda o: isinstance(o, SubModuleNode),
75 75 [filenode_new, filenode_old])
76 76 if submodules:
77 77 return ''
78 78
79 79 for filenode in (filenode_old, filenode_new):
80 80 if not isinstance(filenode, FileNode):
81 81 raise VCSError(
82 82 "Given object should be FileNode object, not %s"
83 83 % filenode.__class__)
84 84
85 85 repo = filenode_new.commit.repository
86 86 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
87 87 new_commit = filenode_new.commit
88 88
89 89 vcs_gitdiff = repo.get_diff(
90 90 old_commit, new_commit, filenode_new.path,
91 91 ignore_whitespace, context, path1=filenode_old.path)
92 92 return vcs_gitdiff
93 93
94 94 NEW_FILENODE = 1
95 95 DEL_FILENODE = 2
96 96 MOD_FILENODE = 3
97 97 RENAMED_FILENODE = 4
98 98 COPIED_FILENODE = 5
99 99 CHMOD_FILENODE = 6
100 100 BIN_FILENODE = 7
101 101
102 102
103 103 class LimitedDiffContainer(object):
104 104
105 105 def __init__(self, diff_limit, cur_diff_size, diff):
106 106 self.diff = diff
107 107 self.diff_limit = diff_limit
108 108 self.cur_diff_size = cur_diff_size
109 109
110 110 def __getitem__(self, key):
111 111 return self.diff.__getitem__(key)
112 112
113 113 def __iter__(self):
114 114 for l in self.diff:
115 115 yield l
116 116
117 117
118 118 class Action(object):
119 119 """
120 120 Contains constants for the action value of the lines in a parsed diff.
121 121 """
122 122
123 123 ADD = 'add'
124 124 DELETE = 'del'
125 125 UNMODIFIED = 'unmod'
126 126
127 127 CONTEXT = 'context'
128 128 OLD_NO_NL = 'old-no-nl'
129 129 NEW_NO_NL = 'new-no-nl'
130 130
131 131
132 132 class DiffProcessor(object):
133 133 """
134 134 Give it a unified or git diff and it returns a list of the files that were
135 135 mentioned in the diff together with a dict of meta information that
136 136 can be used to render it in a HTML template.
137 137
138 138 .. note:: Unicode handling
139 139
140 140 The original diffs are a byte sequence and can contain filenames
141 141 in mixed encodings. This class generally returns `unicode` objects
142 142 since the result is intended for presentation to the user.
143 143
144 144 """
145 145 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
146 146 _newline_marker = re.compile(r'^\\ No newline at end of file')
147 147
148 148 # used for inline highlighter word split
149 149 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
150 150
151 151 # collapse ranges of commits over given number
152 152 _collapse_commits_over = 5
153 153
154 154 def __init__(self, diff, format='gitdiff', diff_limit=None,
155 155 file_limit=None, show_full_diff=True):
156 156 """
157 157 :param diff: A `Diff` object representing a diff from a vcs backend
158 158 :param format: format of diff passed, `udiff` or `gitdiff`
159 159 :param diff_limit: define the size of diff that is considered "big"
160 160 based on that parameter cut off will be triggered, set to None
161 161 to show full diff
162 162 """
163 163 self._diff = diff
164 164 self._format = format
165 165 self.adds = 0
166 166 self.removes = 0
167 167 # calculate diff size
168 168 self.diff_limit = diff_limit
169 169 self.file_limit = file_limit
170 170 self.show_full_diff = show_full_diff
171 171 self.cur_diff_size = 0
172 172 self.parsed = False
173 173 self.parsed_diff = []
174 174
175 175 log.debug('Initialized DiffProcessor with %s mode', format)
176 176 if format == 'gitdiff':
177 177 self.differ = self._highlight_line_difflib
178 178 self._parser = self._parse_gitdiff
179 179 else:
180 180 self.differ = self._highlight_line_udiff
181 181 self._parser = self._new_parse_gitdiff
182 182
183 183 def _copy_iterator(self):
184 184 """
185 185 make a fresh copy of generator, we should not iterate thru
186 186 an original as it's needed for repeating operations on
187 187 this instance of DiffProcessor
188 188 """
189 189 self.__udiff, iterator_copy = tee(self.__udiff)
190 190 return iterator_copy
191 191
192 192 def _escaper(self, string):
193 193 """
194 194 Escaper for diff escapes special chars and checks the diff limit
195 195
196 196 :param string:
197 197 """
198 198 self.cur_diff_size += len(string)
199 199
200 200 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
201 201 raise DiffLimitExceeded('Diff Limit Exceeded')
202 202
203 203 return string \
204 204 .replace('&', '&amp;')\
205 205 .replace('<', '&lt;')\
206 206 .replace('>', '&gt;')
207 207
208 208 def _line_counter(self, l):
209 209 """
210 210 Checks each line and bumps total adds/removes for this diff
211 211
212 212 :param l:
213 213 """
214 214 if l.startswith('+') and not l.startswith('+++'):
215 215 self.adds += 1
216 216 elif l.startswith('-') and not l.startswith('---'):
217 217 self.removes += 1
218 218 return safe_unicode(l)
219 219
220 220 def _highlight_line_difflib(self, line, next_):
221 221 """
222 222 Highlight inline changes in both lines.
223 223 """
224 224
225 225 if line['action'] == Action.DELETE:
226 226 old, new = line, next_
227 227 else:
228 228 old, new = next_, line
229 229
230 230 oldwords = self._token_re.split(old['line'])
231 231 newwords = self._token_re.split(new['line'])
232 232 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
233 233
234 234 oldfragments, newfragments = [], []
235 235 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
236 236 oldfrag = ''.join(oldwords[i1:i2])
237 237 newfrag = ''.join(newwords[j1:j2])
238 238 if tag != 'equal':
239 239 if oldfrag:
240 240 oldfrag = '<del>%s</del>' % oldfrag
241 241 if newfrag:
242 242 newfrag = '<ins>%s</ins>' % newfrag
243 243 oldfragments.append(oldfrag)
244 244 newfragments.append(newfrag)
245 245
246 246 old['line'] = "".join(oldfragments)
247 247 new['line'] = "".join(newfragments)
248 248
249 249 def _highlight_line_udiff(self, line, next_):
250 250 """
251 251 Highlight inline changes in both lines.
252 252 """
253 253 start = 0
254 254 limit = min(len(line['line']), len(next_['line']))
255 255 while start < limit and line['line'][start] == next_['line'][start]:
256 256 start += 1
257 257 end = -1
258 258 limit -= start
259 259 while -end <= limit and line['line'][end] == next_['line'][end]:
260 260 end -= 1
261 261 end += 1
262 262 if start or end:
263 263 def do(l):
264 264 last = end + len(l['line'])
265 265 if l['action'] == Action.ADD:
266 266 tag = 'ins'
267 267 else:
268 268 tag = 'del'
269 269 l['line'] = '%s<%s>%s</%s>%s' % (
270 270 l['line'][:start],
271 271 tag,
272 272 l['line'][start:last],
273 273 tag,
274 274 l['line'][last:]
275 275 )
276 276 do(line)
277 277 do(next_)
278 278
279 279 def _clean_line(self, line, command):
280 280 if command in ['+', '-', ' ']:
281 281 # only modify the line if it's actually a diff thing
282 282 line = line[1:]
283 283 return line
284 284
285 285 def _parse_gitdiff(self, inline_diff=True):
286 286 _files = []
287 287 diff_container = lambda arg: arg
288 288
289 289 for chunk in self._diff.chunks():
290 290 head = chunk.header
291 291
292 292 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
293 293 raw_diff = chunk.raw
294 294 limited_diff = False
295 295 exceeds_limit = False
296 296
297 297 op = None
298 298 stats = {
299 299 'added': 0,
300 300 'deleted': 0,
301 301 'binary': False,
302 302 'ops': {},
303 303 }
304 304
305 305 if head['deleted_file_mode']:
306 306 op = OPS.DEL
307 307 stats['binary'] = True
308 308 stats['ops'][DEL_FILENODE] = 'deleted file'
309 309
310 310 elif head['new_file_mode']:
311 311 op = OPS.ADD
312 312 stats['binary'] = True
313 313 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
314 314 else: # modify operation, can be copy, rename or chmod
315 315
316 316 # CHMOD
317 317 if head['new_mode'] and head['old_mode']:
318 318 op = OPS.MOD
319 319 stats['binary'] = True
320 320 stats['ops'][CHMOD_FILENODE] = (
321 321 'modified file chmod %s => %s' % (
322 322 head['old_mode'], head['new_mode']))
323 323 # RENAME
324 324 if head['rename_from'] != head['rename_to']:
325 325 op = OPS.MOD
326 326 stats['binary'] = True
327 327 stats['ops'][RENAMED_FILENODE] = (
328 328 'file renamed from %s to %s' % (
329 329 head['rename_from'], head['rename_to']))
330 330 # COPY
331 331 if head.get('copy_from') and head.get('copy_to'):
332 332 op = OPS.MOD
333 333 stats['binary'] = True
334 334 stats['ops'][COPIED_FILENODE] = (
335 335 'file copied from %s to %s' % (
336 336 head['copy_from'], head['copy_to']))
337 337
338 338 # If our new parsed headers didn't match anything fallback to
339 339 # old style detection
340 340 if op is None:
341 341 if not head['a_file'] and head['b_file']:
342 342 op = OPS.ADD
343 343 stats['binary'] = True
344 344 stats['ops'][NEW_FILENODE] = 'new file'
345 345
346 346 elif head['a_file'] and not head['b_file']:
347 347 op = OPS.DEL
348 348 stats['binary'] = True
349 349 stats['ops'][DEL_FILENODE] = 'deleted file'
350 350
351 351 # it's not ADD not DELETE
352 352 if op is None:
353 353 op = OPS.MOD
354 354 stats['binary'] = True
355 355 stats['ops'][MOD_FILENODE] = 'modified file'
356 356
357 357 # a real non-binary diff
358 358 if head['a_file'] or head['b_file']:
359 359 try:
360 360 raw_diff, chunks, _stats = self._parse_lines(diff)
361 361 stats['binary'] = False
362 362 stats['added'] = _stats[0]
363 363 stats['deleted'] = _stats[1]
364 364 # explicit mark that it's a modified file
365 365 if op == OPS.MOD:
366 366 stats['ops'][MOD_FILENODE] = 'modified file'
367 367 exceeds_limit = len(raw_diff) > self.file_limit
368 368
369 369 # changed from _escaper function so we validate size of
370 370 # each file instead of the whole diff
371 371 # diff will hide big files but still show small ones
372 372 # from my tests, big files are fairly safe to be parsed
373 373 # but the browser is the bottleneck
374 374 if not self.show_full_diff and exceeds_limit:
375 375 raise DiffLimitExceeded('File Limit Exceeded')
376 376
377 377 except DiffLimitExceeded:
378 378 diff_container = lambda _diff: \
379 379 LimitedDiffContainer(
380 380 self.diff_limit, self.cur_diff_size, _diff)
381 381
382 382 exceeds_limit = len(raw_diff) > self.file_limit
383 383 limited_diff = True
384 384 chunks = []
385 385
386 386 else: # GIT format binary patch, or possibly empty diff
387 387 if head['bin_patch']:
388 388 # we have operation already extracted, but we mark simply
389 389 # it's a diff we wont show for binary files
390 390 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
391 391 chunks = []
392 392
393 393 if chunks and not self.show_full_diff and op == OPS.DEL:
394 394 # if not full diff mode show deleted file contents
395 395 # TODO: anderson: if the view is not too big, there is no way
396 396 # to see the content of the file
397 397 chunks = []
398 398
399 399 chunks.insert(0, [{
400 400 'old_lineno': '',
401 401 'new_lineno': '',
402 402 'action': Action.CONTEXT,
403 403 'line': msg,
404 404 } for _op, msg in stats['ops'].iteritems()
405 405 if _op not in [MOD_FILENODE]])
406 406
407 407 _files.append({
408 408 'filename': safe_unicode(head['b_path']),
409 409 'old_revision': head['a_blob_id'],
410 410 'new_revision': head['b_blob_id'],
411 411 'chunks': chunks,
412 412 'raw_diff': safe_unicode(raw_diff),
413 413 'operation': op,
414 414 'stats': stats,
415 415 'exceeds_limit': exceeds_limit,
416 416 'is_limited_diff': limited_diff,
417 417 })
418 418
419 419 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
420 420 OPS.DEL: 2}.get(info['operation'])
421 421
422 422 if not inline_diff:
423 423 return diff_container(sorted(_files, key=sorter))
424 424
425 425 # highlight inline changes
426 426 for diff_data in _files:
427 427 for chunk in diff_data['chunks']:
428 428 lineiter = iter(chunk)
429 429 try:
430 430 while 1:
431 431 line = lineiter.next()
432 432 if line['action'] not in (
433 433 Action.UNMODIFIED, Action.CONTEXT):
434 434 nextline = lineiter.next()
435 435 if nextline['action'] in ['unmod', 'context'] or \
436 436 nextline['action'] == line['action']:
437 437 continue
438 438 self.differ(line, nextline)
439 439 except StopIteration:
440 440 pass
441 441
442 442 return diff_container(sorted(_files, key=sorter))
443 443
444 444 def _check_large_diff(self):
445 445 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
446 446 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
447 447 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
448 448
449 449 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
450 450 def _new_parse_gitdiff(self, inline_diff=True):
451 451 _files = []
452 452
453 453 # this can be overriden later to a LimitedDiffContainer type
454 454 diff_container = lambda arg: arg
455 455
456 456 for chunk in self._diff.chunks():
457 457 head = chunk.header
458 458 log.debug('parsing diff %r', head)
459 459
460 460 raw_diff = chunk.raw
461 461 limited_diff = False
462 462 exceeds_limit = False
463 463
464 464 op = None
465 465 stats = {
466 466 'added': 0,
467 467 'deleted': 0,
468 468 'binary': False,
469 469 'old_mode': None,
470 470 'new_mode': None,
471 471 'ops': {},
472 472 }
473 473 if head['old_mode']:
474 474 stats['old_mode'] = head['old_mode']
475 475 if head['new_mode']:
476 476 stats['new_mode'] = head['new_mode']
477 477 if head['b_mode']:
478 478 stats['new_mode'] = head['b_mode']
479 479
480 480 # delete file
481 481 if head['deleted_file_mode']:
482 482 op = OPS.DEL
483 483 stats['binary'] = True
484 484 stats['ops'][DEL_FILENODE] = 'deleted file'
485 485
486 486 # new file
487 487 elif head['new_file_mode']:
488 488 op = OPS.ADD
489 489 stats['binary'] = True
490 490 stats['old_mode'] = None
491 491 stats['new_mode'] = head['new_file_mode']
492 492 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
493 493
494 494 # modify operation, can be copy, rename or chmod
495 495 else:
496 496 # CHMOD
497 497 if head['new_mode'] and head['old_mode']:
498 498 op = OPS.MOD
499 499 stats['binary'] = True
500 500 stats['ops'][CHMOD_FILENODE] = (
501 501 'modified file chmod %s => %s' % (
502 502 head['old_mode'], head['new_mode']))
503 503
504 504 # RENAME
505 505 if head['rename_from'] != head['rename_to']:
506 506 op = OPS.MOD
507 507 stats['binary'] = True
508 508 stats['renamed'] = (head['rename_from'], head['rename_to'])
509 509 stats['ops'][RENAMED_FILENODE] = (
510 510 'file renamed from %s to %s' % (
511 511 head['rename_from'], head['rename_to']))
512 512 # COPY
513 513 if head.get('copy_from') and head.get('copy_to'):
514 514 op = OPS.MOD
515 515 stats['binary'] = True
516 516 stats['copied'] = (head['copy_from'], head['copy_to'])
517 517 stats['ops'][COPIED_FILENODE] = (
518 518 'file copied from %s to %s' % (
519 519 head['copy_from'], head['copy_to']))
520 520
521 521 # If our new parsed headers didn't match anything fallback to
522 522 # old style detection
523 523 if op is None:
524 524 if not head['a_file'] and head['b_file']:
525 525 op = OPS.ADD
526 526 stats['binary'] = True
527 527 stats['new_file'] = True
528 528 stats['ops'][NEW_FILENODE] = 'new file'
529 529
530 530 elif head['a_file'] and not head['b_file']:
531 531 op = OPS.DEL
532 532 stats['binary'] = True
533 533 stats['ops'][DEL_FILENODE] = 'deleted file'
534 534
535 535 # it's not ADD not DELETE
536 536 if op is None:
537 537 op = OPS.MOD
538 538 stats['binary'] = True
539 539 stats['ops'][MOD_FILENODE] = 'modified file'
540 540
541 541 # a real non-binary diff
542 542 if head['a_file'] or head['b_file']:
543 543 # simulate splitlines, so we keep the line end part
544 544 diff = self.diff_splitter(chunk.diff)
545 545
546 546 # append each file to the diff size
547 547 raw_chunk_size = len(raw_diff)
548 548
549 549 exceeds_limit = raw_chunk_size > self.file_limit
550 550 self.cur_diff_size += raw_chunk_size
551 551
552 552 try:
553 553 # Check each file instead of the whole diff.
554 554 # Diff will hide big files but still show small ones.
555 555 # From the tests big files are fairly safe to be parsed
556 556 # but the browser is the bottleneck.
557 557 if not self.show_full_diff and exceeds_limit:
558 558 log.debug('File `%s` exceeds current file_limit of %s',
559 559 safe_unicode(head['b_path']), self.file_limit)
560 560 raise DiffLimitExceeded(
561 561 'File Limit %s Exceeded', self.file_limit)
562 562
563 563 self._check_large_diff()
564 564
565 565 raw_diff, chunks, _stats = self._new_parse_lines(diff)
566 566 stats['binary'] = False
567 567 stats['added'] = _stats[0]
568 568 stats['deleted'] = _stats[1]
569 569 # explicit mark that it's a modified file
570 570 if op == OPS.MOD:
571 571 stats['ops'][MOD_FILENODE] = 'modified file'
572 572
573 573 except DiffLimitExceeded:
574 574 diff_container = lambda _diff: \
575 575 LimitedDiffContainer(
576 576 self.diff_limit, self.cur_diff_size, _diff)
577 577
578 578 limited_diff = True
579 579 chunks = []
580 580
581 581 else: # GIT format binary patch, or possibly empty diff
582 582 if head['bin_patch']:
583 583 # we have operation already extracted, but we mark simply
584 584 # it's a diff we wont show for binary files
585 585 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
586 586 chunks = []
587 587
588 588 # Hide content of deleted node by setting empty chunks
589 589 if chunks and not self.show_full_diff and op == OPS.DEL:
590 590 # if not full diff mode show deleted file contents
591 591 # TODO: anderson: if the view is not too big, there is no way
592 592 # to see the content of the file
593 593 chunks = []
594 594
595 595 chunks.insert(
596 596 0, [{'old_lineno': '',
597 597 'new_lineno': '',
598 598 'action': Action.CONTEXT,
599 599 'line': msg,
600 600 } for _op, msg in stats['ops'].iteritems()
601 601 if _op not in [MOD_FILENODE]])
602 602
603 603 original_filename = safe_unicode(head['a_path'])
604 604 _files.append({
605 605 'original_filename': original_filename,
606 606 'filename': safe_unicode(head['b_path']),
607 607 'old_revision': head['a_blob_id'],
608 608 'new_revision': head['b_blob_id'],
609 609 'chunks': chunks,
610 610 'raw_diff': safe_unicode(raw_diff),
611 611 'operation': op,
612 612 'stats': stats,
613 613 'exceeds_limit': exceeds_limit,
614 614 'is_limited_diff': limited_diff,
615 615 })
616 616
617 617 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
618 618 OPS.DEL: 2}.get(info['operation'])
619 619
620 620 return diff_container(sorted(_files, key=sorter))
621 621
622 622 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
623 623 def _parse_lines(self, diff_iter):
624 624 """
625 625 Parse the diff an return data for the template.
626 626 """
627 627
628 628 stats = [0, 0]
629 629 chunks = []
630 630 raw_diff = []
631 631
632 632 try:
633 633 line = diff_iter.next()
634 634
635 635 while line:
636 636 raw_diff.append(line)
637 637 lines = []
638 638 chunks.append(lines)
639 639
640 640 match = self._chunk_re.match(line)
641 641
642 642 if not match:
643 643 break
644 644
645 645 gr = match.groups()
646 646 (old_line, old_end,
647 647 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
648 648 old_line -= 1
649 649 new_line -= 1
650 650
651 651 context = len(gr) == 5
652 652 old_end += old_line
653 653 new_end += new_line
654 654
655 655 if context:
656 656 # skip context only if it's first line
657 657 if int(gr[0]) > 1:
658 658 lines.append({
659 659 'old_lineno': '...',
660 660 'new_lineno': '...',
661 661 'action': Action.CONTEXT,
662 662 'line': line,
663 663 })
664 664
665 665 line = diff_iter.next()
666 666
667 667 while old_line < old_end or new_line < new_end:
668 668 command = ' '
669 669 if line:
670 670 command = line[0]
671 671
672 672 affects_old = affects_new = False
673 673
674 674 # ignore those if we don't expect them
675 675 if command in '#@':
676 676 continue
677 677 elif command == '+':
678 678 affects_new = True
679 679 action = Action.ADD
680 680 stats[0] += 1
681 681 elif command == '-':
682 682 affects_old = True
683 683 action = Action.DELETE
684 684 stats[1] += 1
685 685 else:
686 686 affects_old = affects_new = True
687 687 action = Action.UNMODIFIED
688 688
689 689 if not self._newline_marker.match(line):
690 690 old_line += affects_old
691 691 new_line += affects_new
692 692 lines.append({
693 693 'old_lineno': affects_old and old_line or '',
694 694 'new_lineno': affects_new and new_line or '',
695 695 'action': action,
696 696 'line': self._clean_line(line, command)
697 697 })
698 698 raw_diff.append(line)
699 699
700 700 line = diff_iter.next()
701 701
702 702 if self._newline_marker.match(line):
703 703 # we need to append to lines, since this is not
704 704 # counted in the line specs of diff
705 705 lines.append({
706 706 'old_lineno': '...',
707 707 'new_lineno': '...',
708 708 'action': Action.CONTEXT,
709 709 'line': self._clean_line(line, command)
710 710 })
711 711
712 712 except StopIteration:
713 713 pass
714 714 return ''.join(raw_diff), chunks, stats
715 715
716 716 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
717 717 def _new_parse_lines(self, diff_iter):
718 718 """
719 719 Parse the diff an return data for the template.
720 720 """
721 721
722 722 stats = [0, 0]
723 723 chunks = []
724 724 raw_diff = []
725 725
726 726 try:
727 727 line = diff_iter.next()
728 728
729 729 while line:
730 730 raw_diff.append(line)
731 731 # match header e.g @@ -0,0 +1 @@\n'
732 732 match = self._chunk_re.match(line)
733 733
734 734 if not match:
735 735 break
736 736
737 737 gr = match.groups()
738 738 (old_line, old_end,
739 739 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
740 740
741 741 lines = []
742 742 hunk = {
743 743 'section_header': gr[-1],
744 744 'source_start': old_line,
745 745 'source_length': old_end,
746 746 'target_start': new_line,
747 747 'target_length': new_end,
748 748 'lines': lines,
749 749 }
750 750 chunks.append(hunk)
751 751
752 752 old_line -= 1
753 753 new_line -= 1
754 754
755 755 context = len(gr) == 5
756 756 old_end += old_line
757 757 new_end += new_line
758 758
759 759 line = diff_iter.next()
760 760
761 761 while old_line < old_end or new_line < new_end:
762 762 command = ' '
763 763 if line:
764 764 command = line[0]
765 765
766 766 affects_old = affects_new = False
767 767
768 768 # ignore those if we don't expect them
769 769 if command in '#@':
770 770 continue
771 771 elif command == '+':
772 772 affects_new = True
773 773 action = Action.ADD
774 774 stats[0] += 1
775 775 elif command == '-':
776 776 affects_old = True
777 777 action = Action.DELETE
778 778 stats[1] += 1
779 779 else:
780 780 affects_old = affects_new = True
781 781 action = Action.UNMODIFIED
782 782
783 783 if not self._newline_marker.match(line):
784 784 old_line += affects_old
785 785 new_line += affects_new
786 786 lines.append({
787 787 'old_lineno': affects_old and old_line or '',
788 788 'new_lineno': affects_new and new_line or '',
789 789 'action': action,
790 790 'line': self._clean_line(line, command)
791 791 })
792 792 raw_diff.append(line)
793 793
794 794 line = diff_iter.next()
795 795
796 796 if self._newline_marker.match(line):
797 797 # we need to append to lines, since this is not
798 798 # counted in the line specs of diff
799 799 if affects_old:
800 800 action = Action.OLD_NO_NL
801 801 elif affects_new:
802 802 action = Action.NEW_NO_NL
803 803 else:
804 804 raise Exception('invalid context for no newline')
805 805
806 806 lines.append({
807 807 'old_lineno': None,
808 808 'new_lineno': None,
809 809 'action': action,
810 810 'line': self._clean_line(line, command)
811 811 })
812 812
813 813 except StopIteration:
814 814 pass
815 815
816 816 return ''.join(raw_diff), chunks, stats
817 817
818 818 def _safe_id(self, idstring):
819 819 """Make a string safe for including in an id attribute.
820 820
821 821 The HTML spec says that id attributes 'must begin with
822 822 a letter ([A-Za-z]) and may be followed by any number
823 823 of letters, digits ([0-9]), hyphens ("-"), underscores
824 824 ("_"), colons (":"), and periods (".")'. These regexps
825 825 are slightly over-zealous, in that they remove colons
826 826 and periods unnecessarily.
827 827
828 828 Whitespace is transformed into underscores, and then
829 829 anything which is not a hyphen or a character that
830 830 matches \w (alphanumerics and underscore) is removed.
831 831
832 832 """
833 833 # Transform all whitespace to underscore
834 834 idstring = re.sub(r'\s', "_", '%s' % idstring)
835 835 # Remove everything that is not a hyphen or a member of \w
836 836 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
837 837 return idstring
838 838
839 839 @classmethod
840 840 def diff_splitter(cls, string):
841 841 """
842 842 Diff split that emulates .splitlines() but works only on \n
843 843 """
844 844 if not string:
845 845 return
846 846 elif string == '\n':
847 847 yield u'\n'
848 848 else:
849 849
850 850 has_newline = string.endswith('\n')
851 851 elements = string.split('\n')
852 852 if has_newline:
853 853 # skip last element as it's empty string from newlines
854 854 elements = elements[:-1]
855 855
856 856 len_elements = len(elements)
857 857
858 858 for cnt, line in enumerate(elements, start=1):
859 859 last_line = cnt == len_elements
860 860 if last_line and not has_newline:
861 861 yield safe_unicode(line)
862 862 else:
863 863 yield safe_unicode(line) + '\n'
864 864
865 865 def prepare(self, inline_diff=True):
866 866 """
867 867 Prepare the passed udiff for HTML rendering.
868 868
869 869 :return: A list of dicts with diff information.
870 870 """
871 871 parsed = self._parser(inline_diff=inline_diff)
872 872 self.parsed = True
873 873 self.parsed_diff = parsed
874 874 return parsed
875 875
876 876 def as_raw(self, diff_lines=None):
877 877 """
878 878 Returns raw diff as a byte string
879 879 """
880 880 return self._diff.raw
881 881
882 882 def as_html(self, table_class='code-difftable', line_class='line',
883 883 old_lineno_class='lineno old', new_lineno_class='lineno new',
884 884 code_class='code', enable_comments=False, parsed_lines=None):
885 885 """
886 886 Return given diff as html table with customized css classes
887 887 """
888 888 # TODO(marcink): not sure how to pass in translator
889 889 # here in an efficient way, leave the _ for proper gettext extraction
890 890 _ = lambda s: s
891 891
892 892 def _link_to_if(condition, label, url):
893 893 """
894 894 Generates a link if condition is meet or just the label if not.
895 895 """
896 896
897 897 if condition:
898 898 return '''<a href="%(url)s" class="tooltip"
899 899 title="%(title)s">%(label)s</a>''' % {
900 900 'title': _('Click to select line'),
901 901 'url': url,
902 902 'label': label
903 903 }
904 904 else:
905 905 return label
906 906 if not self.parsed:
907 907 self.prepare()
908 908
909 909 diff_lines = self.parsed_diff
910 910 if parsed_lines:
911 911 diff_lines = parsed_lines
912 912
913 913 _html_empty = True
914 914 _html = []
915 915 _html.append('''<table class="%(table_class)s">\n''' % {
916 916 'table_class': table_class
917 917 })
918 918
919 919 for diff in diff_lines:
920 920 for line in diff['chunks']:
921 921 _html_empty = False
922 922 for change in line:
923 923 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
924 924 'lc': line_class,
925 925 'action': change['action']
926 926 })
927 927 anchor_old_id = ''
928 928 anchor_new_id = ''
929 929 anchor_old = "%(filename)s_o%(oldline_no)s" % {
930 930 'filename': self._safe_id(diff['filename']),
931 931 'oldline_no': change['old_lineno']
932 932 }
933 933 anchor_new = "%(filename)s_n%(oldline_no)s" % {
934 934 'filename': self._safe_id(diff['filename']),
935 935 'oldline_no': change['new_lineno']
936 936 }
937 937 cond_old = (change['old_lineno'] != '...' and
938 938 change['old_lineno'])
939 939 cond_new = (change['new_lineno'] != '...' and
940 940 change['new_lineno'])
941 941 if cond_old:
942 942 anchor_old_id = 'id="%s"' % anchor_old
943 943 if cond_new:
944 944 anchor_new_id = 'id="%s"' % anchor_new
945 945
946 946 if change['action'] != Action.CONTEXT:
947 947 anchor_link = True
948 948 else:
949 949 anchor_link = False
950 950
951 951 ###########################################################
952 952 # COMMENT ICONS
953 953 ###########################################################
954 954 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
955 955
956 956 if enable_comments and change['action'] != Action.CONTEXT:
957 957 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
958 958
959 959 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
960 960
961 961 ###########################################################
962 962 # OLD LINE NUMBER
963 963 ###########################################################
964 964 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
965 965 'a_id': anchor_old_id,
966 966 'olc': old_lineno_class
967 967 })
968 968
969 969 _html.append('''%(link)s''' % {
970 970 'link': _link_to_if(anchor_link, change['old_lineno'],
971 971 '#%s' % anchor_old)
972 972 })
973 973 _html.append('''</td>\n''')
974 974 ###########################################################
975 975 # NEW LINE NUMBER
976 976 ###########################################################
977 977
978 978 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
979 979 'a_id': anchor_new_id,
980 980 'nlc': new_lineno_class
981 981 })
982 982
983 983 _html.append('''%(link)s''' % {
984 984 'link': _link_to_if(anchor_link, change['new_lineno'],
985 985 '#%s' % anchor_new)
986 986 })
987 987 _html.append('''</td>\n''')
988 988 ###########################################################
989 989 # CODE
990 990 ###########################################################
991 991 code_classes = [code_class]
992 992 if (not enable_comments or
993 993 change['action'] == Action.CONTEXT):
994 994 code_classes.append('no-comment')
995 995 _html.append('\t<td class="%s">' % ' '.join(code_classes))
996 996 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
997 997 'code': change['line']
998 998 })
999 999
1000 1000 _html.append('''\t</td>''')
1001 1001 _html.append('''\n</tr>\n''')
1002 1002 _html.append('''</table>''')
1003 1003 if _html_empty:
1004 1004 return None
1005 1005 return ''.join(_html)
1006 1006
1007 1007 def stat(self):
1008 1008 """
1009 1009 Returns tuple of added, and removed lines for this instance
1010 1010 """
1011 1011 return self.adds, self.removes
1012 1012
1013 1013 def get_context_of_line(
1014 1014 self, path, diff_line=None, context_before=3, context_after=3):
1015 1015 """
1016 1016 Returns the context lines for the specified diff line.
1017 1017
1018 1018 :type diff_line: :class:`DiffLineNumber`
1019 1019 """
1020 1020 assert self.parsed, "DiffProcessor is not initialized."
1021 1021
1022 1022 if None not in diff_line:
1023 1023 raise ValueError(
1024 1024 "Cannot specify both line numbers: {}".format(diff_line))
1025 1025
1026 1026 file_diff = self._get_file_diff(path)
1027 1027 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1028 1028
1029 1029 first_line_to_include = max(idx - context_before, 0)
1030 1030 first_line_after_context = idx + context_after + 1
1031 1031 context_lines = chunk[first_line_to_include:first_line_after_context]
1032 1032
1033 1033 line_contents = [
1034 1034 _context_line(line) for line in context_lines
1035 1035 if _is_diff_content(line)]
1036 1036 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1037 1037 # Once they are fixed, we can drop this line here.
1038 1038 if line_contents:
1039 1039 line_contents[-1] = (
1040 1040 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1041 1041 return line_contents
1042 1042
1043 1043 def find_context(self, path, context, offset=0):
1044 1044 """
1045 1045 Finds the given `context` inside of the diff.
1046 1046
1047 1047 Use the parameter `offset` to specify which offset the target line has
1048 1048 inside of the given `context`. This way the correct diff line will be
1049 1049 returned.
1050 1050
1051 1051 :param offset: Shall be used to specify the offset of the main line
1052 1052 within the given `context`.
1053 1053 """
1054 1054 if offset < 0 or offset >= len(context):
1055 1055 raise ValueError(
1056 1056 "Only positive values up to the length of the context "
1057 1057 "minus one are allowed.")
1058 1058
1059 1059 matches = []
1060 1060 file_diff = self._get_file_diff(path)
1061 1061
1062 1062 for chunk in file_diff['chunks']:
1063 1063 context_iter = iter(context)
1064 1064 for line_idx, line in enumerate(chunk):
1065 1065 try:
1066 1066 if _context_line(line) == context_iter.next():
1067 1067 continue
1068 1068 except StopIteration:
1069 1069 matches.append((line_idx, chunk))
1070 1070 context_iter = iter(context)
1071 1071
1072 1072 # Increment position and triger StopIteration
1073 1073 # if we had a match at the end
1074 1074 line_idx += 1
1075 1075 try:
1076 1076 context_iter.next()
1077 1077 except StopIteration:
1078 1078 matches.append((line_idx, chunk))
1079 1079
1080 1080 effective_offset = len(context) - offset
1081 1081 found_at_diff_lines = [
1082 1082 _line_to_diff_line_number(chunk[idx - effective_offset])
1083 1083 for idx, chunk in matches]
1084 1084
1085 1085 return found_at_diff_lines
1086 1086
1087 1087 def _get_file_diff(self, path):
1088 1088 for file_diff in self.parsed_diff:
1089 1089 if file_diff['filename'] == path:
1090 1090 break
1091 1091 else:
1092 1092 raise FileNotInDiffException("File {} not in diff".format(path))
1093 1093 return file_diff
1094 1094
1095 1095 def _find_chunk_line_index(self, file_diff, diff_line):
1096 1096 for chunk in file_diff['chunks']:
1097 1097 for idx, line in enumerate(chunk):
1098 1098 if line['old_lineno'] == diff_line.old:
1099 1099 return chunk, idx
1100 1100 if line['new_lineno'] == diff_line.new:
1101 1101 return chunk, idx
1102 1102 raise LineNotInDiffException(
1103 1103 "The line {} is not part of the diff.".format(diff_line))
1104 1104
1105 1105
1106 1106 def _is_diff_content(line):
1107 1107 return line['action'] in (
1108 1108 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1109 1109
1110 1110
1111 1111 def _context_line(line):
1112 1112 return (line['action'], line['line'])
1113 1113
1114 1114
1115 1115 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1116 1116
1117 1117
1118 1118 def _line_to_diff_line_number(line):
1119 1119 new_line_no = line['new_lineno'] or None
1120 1120 old_line_no = line['old_lineno'] or None
1121 1121 return DiffLineNumber(old=old_line_no, new=new_line_no)
1122 1122
1123 1123
1124 1124 class FileNotInDiffException(Exception):
1125 1125 """
1126 1126 Raised when the context for a missing file is requested.
1127 1127
1128 1128 If you request the context for a line in a file which is not part of the
1129 1129 given diff, then this exception is raised.
1130 1130 """
1131 1131
1132 1132
1133 1133 class LineNotInDiffException(Exception):
1134 1134 """
1135 1135 Raised when the context for a missing line is requested.
1136 1136
1137 1137 If you request the context for a line in a file and this line is not
1138 1138 part of the given diff, then this exception is raised.
1139 1139 """
1140 1140
1141 1141
1142 1142 class DiffLimitExceeded(Exception):
1143 1143 pass
1144 1144
1145 1145
1146 1146 # NOTE(marcink): if diffs.mako change, probably this
1147 1147 # needs a bump to next version
1148 CURRENT_DIFF_VERSION = 'v3'
1148 CURRENT_DIFF_VERSION = 'v4'
1149 1149
1150 1150
1151 1151 def _cleanup_cache_file(cached_diff_file):
1152 1152 # cleanup file to not store it "damaged"
1153 1153 try:
1154 1154 os.remove(cached_diff_file)
1155 1155 except Exception:
1156 1156 log.exception('Failed to cleanup path %s', cached_diff_file)
1157 1157
1158 1158
1159 1159 def cache_diff(cached_diff_file, diff, commits):
1160 1160
1161 1161 struct = {
1162 1162 'version': CURRENT_DIFF_VERSION,
1163 1163 'diff': diff,
1164 1164 'commits': commits
1165 1165 }
1166 1166
1167 1167 try:
1168 1168 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1169 1169 pickle.dump(struct, f)
1170 1170 log.debug('Saved diff cache under %s', cached_diff_file)
1171 1171 except Exception:
1172 1172 log.warn('Failed to save cache', exc_info=True)
1173 1173 _cleanup_cache_file(cached_diff_file)
1174 1174
1175 1175
1176 1176 def load_cached_diff(cached_diff_file):
1177 1177
1178 1178 default_struct = {
1179 1179 'version': CURRENT_DIFF_VERSION,
1180 1180 'diff': None,
1181 1181 'commits': None
1182 1182 }
1183 1183
1184 1184 has_cache = os.path.isfile(cached_diff_file)
1185 1185 if not has_cache:
1186 1186 return default_struct
1187 1187
1188 1188 data = None
1189 1189 try:
1190 1190 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1191 1191 data = pickle.load(f)
1192 1192 log.debug('Loaded diff cache from %s', cached_diff_file)
1193 1193 except Exception:
1194 1194 log.warn('Failed to read diff cache file', exc_info=True)
1195 1195
1196 1196 if not data:
1197 1197 data = default_struct
1198 1198
1199 1199 if not isinstance(data, dict):
1200 1200 # old version of data ?
1201 1201 data = default_struct
1202 1202
1203 1203 # check version
1204 1204 if data.get('version') != CURRENT_DIFF_VERSION:
1205 1205 # purge cache
1206 1206 _cleanup_cache_file(cached_diff_file)
1207 1207 return default_struct
1208 1208
1209 1209 return data
1210 1210
1211 1211
1212 1212 def generate_diff_cache_key(*args):
1213 1213 """
1214 1214 Helper to generate a cache key using arguments
1215 1215 """
1216 1216 def arg_mapper(input_param):
1217 1217 input_param = safe_str(input_param)
1218 1218 # we cannot allow '/' in arguments since it would allow
1219 1219 # subdirectory usage
1220 1220 input_param.replace('/', '_')
1221 1221 return input_param or None # prevent empty string arguments
1222 1222
1223 1223 return '_'.join([
1224 1224 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1225 1225
1226 1226
1227 1227 def diff_cache_exist(cache_storage, *args):
1228 1228 """
1229 1229 Based on all generated arguments check and return a cache path
1230 1230 """
1231 1231 cache_key = generate_diff_cache_key(*args)
1232 1232 cache_file_path = os.path.join(cache_storage, cache_key)
1233 1233 # prevent path traversal attacks using some param that have e.g '../../'
1234 1234 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1235 1235 raise ValueError('Final path must be within {}'.format(cache_storage))
1236 1236
1237 1237 return cache_file_path
@@ -1,1231 +1,1242 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21
22 22 div.diffblock .sidebyside {
23 23 background: #ffffff;
24 24 }
25 25
26 26 div.diffblock {
27 27 overflow-x: auto;
28 28 overflow-y: hidden;
29 29 clear: both;
30 30 padding: 0px;
31 31 background: @grey6;
32 32 border: @border-thickness solid @grey5;
33 33 -webkit-border-radius: @border-radius @border-radius 0px 0px;
34 34 border-radius: @border-radius @border-radius 0px 0px;
35 35
36 36
37 37 .comments-number {
38 38 float: right;
39 39 }
40 40
41 41 // BEGIN CODE-HEADER STYLES
42 42
43 43 .code-header {
44 44 background: @grey6;
45 45 padding: 10px 0 10px 0;
46 46 height: auto;
47 47 width: 100%;
48 48
49 49 .hash {
50 50 float: left;
51 51 padding: 2px 0 0 2px;
52 52 }
53 53
54 54 .date {
55 55 float: left;
56 56 text-transform: uppercase;
57 57 padding: 4px 0px 0px 2px;
58 58 }
59 59
60 60 div {
61 61 margin-left: 4px;
62 62 }
63 63
64 64 div.compare_header {
65 65 min-height: 40px;
66 66 margin: 0;
67 67 padding: 0 @padding;
68 68
69 69 .drop-menu {
70 70 float:left;
71 71 display: block;
72 72 margin:0 0 @padding 0;
73 73 }
74 74
75 75 .compare-label {
76 76 float: left;
77 77 clear: both;
78 78 display: inline-block;
79 79 min-width: 5em;
80 80 margin: 0;
81 81 padding: @button-padding @button-padding @button-padding 0;
82 82 font-family: @text-semibold;
83 83 }
84 84
85 85 .compare-buttons {
86 86 float: left;
87 87 margin: 0;
88 88 padding: 0 0 @padding;
89 89
90 90 .btn {
91 91 margin: 0 @padding 0 0;
92 92 }
93 93 }
94 94 }
95 95
96 96 }
97 97
98 98 .parents {
99 99 float: left;
100 100 width: 100px;
101 101 font-weight: 400;
102 102 vertical-align: middle;
103 103 padding: 0px 2px 0px 2px;
104 104 background-color: @grey6;
105 105
106 106 #parent_link {
107 107 margin: 00px 2px;
108 108
109 109 &.double {
110 110 margin: 0px 2px;
111 111 }
112 112
113 113 &.disabled{
114 114 margin-right: @padding;
115 115 }
116 116 }
117 117 }
118 118
119 119 .children {
120 120 float: right;
121 121 width: 100px;
122 122 font-weight: 400;
123 123 vertical-align: middle;
124 124 text-align: right;
125 125 padding: 0px 2px 0px 2px;
126 126 background-color: @grey6;
127 127
128 128 #child_link {
129 129 margin: 0px 2px;
130 130
131 131 &.double {
132 132 margin: 0px 2px;
133 133 }
134 134
135 135 &.disabled{
136 136 margin-right: @padding;
137 137 }
138 138 }
139 139 }
140 140
141 141 .changeset_header {
142 142 height: 16px;
143 143
144 144 & > div{
145 145 margin-right: @padding;
146 146 }
147 147 }
148 148
149 149 .changeset_file {
150 150 text-align: left;
151 151 float: left;
152 152 padding: 0;
153 153
154 154 a{
155 155 display: inline-block;
156 156 margin-right: 0.5em;
157 157 }
158 158
159 159 #selected_mode{
160 160 margin-left: 0;
161 161 }
162 162 }
163 163
164 164 .diff-menu-wrapper {
165 165 float: left;
166 166 }
167 167
168 168 .diff-menu {
169 169 position: absolute;
170 170 background: none repeat scroll 0 0 #FFFFFF;
171 171 border-color: #003367 @grey3 @grey3;
172 172 border-right: 1px solid @grey3;
173 173 border-style: solid solid solid;
174 174 border-width: @border-thickness;
175 175 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
176 176 margin-top: 5px;
177 177 margin-left: 1px;
178 178 }
179 179
180 180 .diff-actions, .editor-actions {
181 181 float: left;
182 182
183 183 input{
184 184 margin: 0 0.5em 0 0;
185 185 }
186 186 }
187 187
188 188 // END CODE-HEADER STYLES
189 189
190 190 // BEGIN CODE-BODY STYLES
191 191
192 192 .code-body {
193 193 padding: 0;
194 194 background-color: #ffffff;
195 195 position: relative;
196 196 max-width: none;
197 197 box-sizing: border-box;
198 198 // TODO: johbo: Parent has overflow: auto, this forces the child here
199 199 // to have the intended size and to scroll. Should be simplified.
200 200 width: 100%;
201 201 overflow-x: auto;
202 202 }
203 203
204 204 pre.raw {
205 205 background: white;
206 206 color: @grey1;
207 207 }
208 208 // END CODE-BODY STYLES
209 209
210 210 }
211 211
212 212
213 213 table.code-difftable {
214 214 border-collapse: collapse;
215 215 width: 99%;
216 216 border-radius: 0px !important;
217 217
218 218 td {
219 219 padding: 0 !important;
220 220 background: none !important;
221 221 border: 0 !important;
222 222 }
223 223
224 224 .context {
225 225 background: none repeat scroll 0 0 #DDE7EF;
226 226 }
227 227
228 228 .add {
229 229 background: none repeat scroll 0 0 #DDFFDD;
230 230
231 231 ins {
232 232 background: none repeat scroll 0 0 #AAFFAA;
233 233 text-decoration: none;
234 234 }
235 235 }
236 236
237 237 .del {
238 238 background: none repeat scroll 0 0 #FFDDDD;
239 239
240 240 del {
241 241 background: none repeat scroll 0 0 #FFAAAA;
242 242 text-decoration: none;
243 243 }
244 244 }
245 245
246 246 /** LINE NUMBERS **/
247 247 .lineno {
248 248 padding-left: 2px !important;
249 249 padding-right: 2px;
250 250 text-align: right;
251 251 width: 32px;
252 252 -moz-user-select: none;
253 253 -webkit-user-select: none;
254 254 border-right: @border-thickness solid @grey5 !important;
255 255 border-left: 0px solid #CCC !important;
256 256 border-top: 0px solid #CCC !important;
257 257 border-bottom: none !important;
258 258
259 259 a {
260 260 &:extend(pre);
261 261 text-align: right;
262 262 padding-right: 2px;
263 263 cursor: pointer;
264 264 display: block;
265 265 width: 32px;
266 266 }
267 267 }
268 268
269 269 .context {
270 270 cursor: auto;
271 271 &:extend(pre);
272 272 }
273 273
274 274 .lineno-inline {
275 275 background: none repeat scroll 0 0 #FFF !important;
276 276 padding-left: 2px;
277 277 padding-right: 2px;
278 278 text-align: right;
279 279 width: 30px;
280 280 -moz-user-select: none;
281 281 -webkit-user-select: none;
282 282 }
283 283
284 284 /** CODE **/
285 285 .code {
286 286 display: block;
287 287 width: 100%;
288 288
289 289 td {
290 290 margin: 0;
291 291 padding: 0;
292 292 }
293 293
294 294 pre {
295 295 margin: 0;
296 296 padding: 0;
297 297 margin-left: .5em;
298 298 }
299 299 }
300 300 }
301 301
302 302
303 303 // Comments
304 304
305 305 div.comment:target {
306 306 border-left: 6px solid @comment-highlight-color !important;
307 307 padding-left: 3px;
308 308 margin-left: -9px;
309 309 }
310 310
311 311 //TODO: anderson: can't get an absolute number out of anything, so had to put the
312 312 //current values that might change. But to make it clear I put as a calculation
313 313 @comment-max-width: 1065px;
314 314 @pr-extra-margin: 34px;
315 315 @pr-border-spacing: 4px;
316 316 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
317 317
318 318 // Pull Request
319 319 .cs_files .code-difftable {
320 320 border: @border-thickness solid @grey5; //borders only on PRs
321 321
322 322 .comment-inline-form,
323 323 div.comment {
324 324 width: @pr-comment-width;
325 325 }
326 326 }
327 327
328 328 // Changeset
329 329 .code-difftable {
330 330 .comment-inline-form,
331 331 div.comment {
332 332 width: @comment-max-width;
333 333 }
334 334 }
335 335
336 336 //Style page
337 337 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
338 338 #style-page .code-difftable{
339 339 .comment-inline-form,
340 340 div.comment {
341 341 width: @comment-max-width - @style-extra-margin;
342 342 }
343 343 }
344 344
345 345 #context-bar > h2 {
346 346 font-size: 20px;
347 347 }
348 348
349 349 #context-bar > h2> a {
350 350 font-size: 20px;
351 351 }
352 352 // end of defaults
353 353
354 354 .file_diff_buttons {
355 355 padding: 0 0 @padding;
356 356
357 357 .drop-menu {
358 358 float: left;
359 359 margin: 0 @padding 0 0;
360 360 }
361 361 .btn {
362 362 margin: 0 @padding 0 0;
363 363 }
364 364 }
365 365
366 366 .code-body.textarea.editor {
367 367 max-width: none;
368 368 padding: 15px;
369 369 }
370 370
371 371 td.injected_diff{
372 372 max-width: 1178px;
373 373 overflow-x: auto;
374 374 overflow-y: hidden;
375 375
376 376 div.diff-container,
377 377 div.diffblock{
378 378 max-width: 100%;
379 379 }
380 380
381 381 div.code-body {
382 382 max-width: 1124px;
383 383 overflow-x: auto;
384 384 overflow-y: hidden;
385 385 padding: 0;
386 386 }
387 387 div.diffblock {
388 388 border: none;
389 389 }
390 390
391 391 &.inline-form {
392 392 width: 99%
393 393 }
394 394 }
395 395
396 396
397 397 table.code-difftable {
398 398 width: 100%;
399 399 }
400 400
401 401 /** PYGMENTS COLORING **/
402 402 div.codeblock {
403 403
404 404 // TODO: johbo: Added interim to get rid of the margin around
405 405 // Select2 widgets. This needs further cleanup.
406 406 margin-top: @padding;
407 407
408 408 overflow: auto;
409 409 padding: 0px;
410 410 border: @border-thickness solid @grey5;
411 411 background: @grey6;
412 412 .border-radius(@border-radius);
413 413
414 414 #remove_gist {
415 415 float: right;
416 416 }
417 417
418 418 .gist_url {
419 419 padding: 0px 0px 10px 0px;
420 420 }
421 421
422 422 .author {
423 423 clear: both;
424 424 vertical-align: middle;
425 425 font-family: @text-bold;
426 426 }
427 427
428 428 .btn-mini {
429 429 float: left;
430 430 margin: 0 5px 0 0;
431 431 }
432 432
433 433 .code-header {
434 434 padding: @padding;
435 435 border-bottom: @border-thickness solid @grey5;
436 436
437 437 .rc-user {
438 438 min-width: 0;
439 439 margin-right: .5em;
440 440 }
441 441
442 442 .stats {
443 443 clear: both;
444 444 margin: 0 0 @padding 0;
445 445 padding: 0;
446 446 .left {
447 447 float: left;
448 448 clear: left;
449 449 max-width: 75%;
450 450 margin: 0 0 @padding 0;
451 451
452 452 &.item {
453 453 margin-right: @padding;
454 454 &.last { border-right: none; }
455 455 }
456 456 }
457 457 .buttons { float: right; }
458 458 .author {
459 459 height: 25px; margin-left: 15px; font-weight: bold;
460 460 }
461 461 }
462 462
463 463 .commit {
464 464 margin: 5px 0 0 26px;
465 465 font-weight: normal;
466 466 white-space: pre-wrap;
467 467 }
468 468 }
469 469
470 470 .message {
471 471 position: relative;
472 472 margin: @padding;
473 473
474 474 .codeblock-label {
475 475 margin: 0 0 1em 0;
476 476 }
477 477 }
478 478
479 479 .code-body {
480 480 padding: @padding;
481 481 background-color: #ffffff;
482 482 min-width: 100%;
483 483 box-sizing: border-box;
484 484 // TODO: johbo: Parent has overflow: auto, this forces the child here
485 485 // to have the intended size and to scroll. Should be simplified.
486 486 width: 100%;
487 487 overflow-x: auto;
488 488
489 489 img.rendered-binary {
490 490 height: auto;
491 491 width: 100%;
492 492 }
493 493 }
494 494 }
495 495
496 496 .code-highlighttable,
497 497 div.codeblock {
498 498
499 499 &.readme {
500 500 background-color: white;
501 501 }
502 502
503 503 .markdown-block table {
504 504 border-collapse: collapse;
505 505
506 506 th,
507 507 td {
508 508 padding: .5em;
509 509 border: @border-thickness solid @border-default-color;
510 510 }
511 511 }
512 512
513 513 table {
514 514 border: 0px;
515 515 margin: 0;
516 516 letter-spacing: normal;
517 517
518 518
519 519 td {
520 520 border: 0px;
521 521 vertical-align: top;
522 522 }
523 523 }
524 524 }
525 525
526 526 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
527 527 div.search-code-body {
528 528 background-color: #ffffff; padding: 5px 0 5px 10px;
529 529 pre {
530 530 .match { background-color: #faffa6;}
531 531 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
532 532 }
533 533 .code-highlighttable {
534 534 border-collapse: collapse;
535 535
536 536 tr:hover {
537 537 background: #fafafa;
538 538 }
539 539 td.code {
540 540 padding-left: 10px;
541 541 }
542 542 td.line {
543 543 border-right: 1px solid #ccc !important;
544 544 padding-right: 10px;
545 545 text-align: right;
546 546 font-family: "Lucida Console",Monaco,monospace;
547 547 span {
548 548 white-space: pre-wrap;
549 549 color: #666666;
550 550 }
551 551 }
552 552 }
553 553 }
554 554
555 555 div.annotatediv { margin-left: 2px; margin-right: 4px; }
556 556 .code-highlight {
557 557 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
558 558 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
559 559 pre div:target {background-color: @comment-highlight-color !important;}
560 560 }
561 561
562 562 .linenos a { text-decoration: none; }
563 563
564 564 .CodeMirror-selected { background: @rchighlightblue; }
565 565 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
566 566 .CodeMirror ::selection { background: @rchighlightblue; }
567 567 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
568 568
569 569 .code { display: block; border:0px !important; }
570 570 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
571 571 .codehilite {
572 572 .hll { background-color: #ffffcc }
573 573 .c { color: #408080; font-style: italic } /* Comment */
574 574 .err, .codehilite .err { border: none } /* Error */
575 575 .k { color: #008000; font-weight: bold } /* Keyword */
576 576 .o { color: #666666 } /* Operator */
577 577 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
578 578 .cp { color: #BC7A00 } /* Comment.Preproc */
579 579 .c1 { color: #408080; font-style: italic } /* Comment.Single */
580 580 .cs { color: #408080; font-style: italic } /* Comment.Special */
581 581 .gd { color: #A00000 } /* Generic.Deleted */
582 582 .ge { font-style: italic } /* Generic.Emph */
583 583 .gr { color: #FF0000 } /* Generic.Error */
584 584 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
585 585 .gi { color: #00A000 } /* Generic.Inserted */
586 586 .go { color: #808080 } /* Generic.Output */
587 587 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
588 588 .gs { font-weight: bold } /* Generic.Strong */
589 589 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
590 590 .gt { color: #0040D0 } /* Generic.Traceback */
591 591 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
592 592 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
593 593 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
594 594 .kp { color: #008000 } /* Keyword.Pseudo */
595 595 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
596 596 .kt { color: #B00040 } /* Keyword.Type */
597 597 .m { color: #666666 } /* Literal.Number */
598 598 .s { color: #BA2121 } /* Literal.String */
599 599 .na { color: #7D9029 } /* Name.Attribute */
600 600 .nb { color: #008000 } /* Name.Builtin */
601 601 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
602 602 .no { color: #880000 } /* Name.Constant */
603 603 .nd { color: #AA22FF } /* Name.Decorator */
604 604 .ni { color: #999999; font-weight: bold } /* Name.Entity */
605 605 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
606 606 .nf { color: #0000FF } /* Name.Function */
607 607 .nl { color: #A0A000 } /* Name.Label */
608 608 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
609 609 .nt { color: #008000; font-weight: bold } /* Name.Tag */
610 610 .nv { color: #19177C } /* Name.Variable */
611 611 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
612 612 .w { color: #bbbbbb } /* Text.Whitespace */
613 613 .mf { color: #666666 } /* Literal.Number.Float */
614 614 .mh { color: #666666 } /* Literal.Number.Hex */
615 615 .mi { color: #666666 } /* Literal.Number.Integer */
616 616 .mo { color: #666666 } /* Literal.Number.Oct */
617 617 .sb { color: #BA2121 } /* Literal.String.Backtick */
618 618 .sc { color: #BA2121 } /* Literal.String.Char */
619 619 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
620 620 .s2 { color: #BA2121 } /* Literal.String.Double */
621 621 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
622 622 .sh { color: #BA2121 } /* Literal.String.Heredoc */
623 623 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
624 624 .sx { color: #008000 } /* Literal.String.Other */
625 625 .sr { color: #BB6688 } /* Literal.String.Regex */
626 626 .s1 { color: #BA2121 } /* Literal.String.Single */
627 627 .ss { color: #19177C } /* Literal.String.Symbol */
628 628 .bp { color: #008000 } /* Name.Builtin.Pseudo */
629 629 .vc { color: #19177C } /* Name.Variable.Class */
630 630 .vg { color: #19177C } /* Name.Variable.Global */
631 631 .vi { color: #19177C } /* Name.Variable.Instance */
632 632 .il { color: #666666 } /* Literal.Number.Integer.Long */
633 633 }
634 634
635 635 /* customized pre blocks for markdown/rst */
636 636 pre.literal-block, .codehilite pre{
637 637 padding: @padding;
638 638 border: 1px solid @grey6;
639 639 .border-radius(@border-radius);
640 640 background-color: @grey7;
641 641 }
642 642
643 643
644 644 /* START NEW CODE BLOCK CSS */
645 645
646 646 @cb-line-height: 18px;
647 647 @cb-line-code-padding: 10px;
648 648 @cb-text-padding: 5px;
649 649
650 650 @pill-padding: 2px 7px;
651 651 @pill-padding-small: 2px 2px 1px 2px;
652 652
653 653 input.filediff-collapse-state {
654 654 display: none;
655 655
656 656 &:checked + .filediff { /* file diff is collapsed */
657 657 .cb {
658 658 display: none
659 659 }
660 660 .filediff-collapse-indicator {
661 661 width: 0;
662 662 height: 0;
663 663 border-style: solid;
664 664 border-width: 4.5px 0 4.5px 9.3px;
665 665 border-color: transparent transparent transparent #aaa;
666 666 margin: 6px 0px;
667 667 }
668 668 .filediff-menu {
669 669 display: none;
670 670 }
671 671
672 672 }
673 673
674 674 &+ .filediff { /* file diff is expanded */
675 675 .filediff-collapse-indicator {
676 676 width: 0;
677 677 height: 0;
678 678 border-style: solid;
679 679 border-width: 9.3px 4.5px 0 4.5px;
680 680 border-color: #aaa transparent transparent transparent;
681 681 margin: 6px 0px;
682 682
683 683 }
684 684 .filediff-menu {
685 685 display: block;
686 686 }
687 687 margin: 10px 0;
688 688 &:nth-child(2) {
689 689 margin: 0;
690 690 }
691 691 }
692 692 }
693 693
694 694 .filediffs .anchor {
695 695 display: block;
696 696 height: 40px;
697 697 margin-top: -40px;
698 698 visibility: hidden;
699 699 }
700 700
701 701 .filediffs .anchor:nth-of-type(1) {
702 702 display: block;
703 703 height: 80px;
704 704 margin-top: -80px;
705 705 visibility: hidden;
706 706 }
707 707
708 708 .cs_files {
709 709 clear: both;
710 710 }
711 711
712 712 #diff-file-sticky{
713 713 will-change: min-height;
714 714 }
715 715
716 716 .sidebar__inner{
717 717 transform: translate(0, 0); /* For browsers don't support translate3d. */
718 718 transform: translate3d(0, 0, 0);
719 719 will-change: position, transform;
720 720 height: 70px;
721 721 z-index: 30;
722 722 background-color: #fff;
723 723 padding: 5px 0px;
724 724 }
725 725
726 726 .sidebar__bar {
727 727 padding: 5px 0px 0px 0px
728 728 }
729 729
730 730 .fpath-placeholder {
731 731 clear: both;
732 732 visibility: hidden
733 733 }
734 734
735 735 .is-affixed {
736 736 .sidebar_inner_shadow {
737 737 position: fixed;
738 738 top: 75px;
739 739 right: -100%;
740 740 left: -100%;
741 741 z-index: 28;
742 742 display: block;
743 743 height: 5px;
744 744 content: "";
745 745 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
746 746 border-top: 1px solid rgba(0, 0, 0, 0.15);
747 747 }
748 748 .fpath-placeholder {
749 749 visibility: visible !important;
750 750 }
751 751 }
752 752
753 753 .diffset-menu {
754 754 margin-bottom: 20px;
755 755 }
756 756 .diffset {
757 757 margin: 20px auto;
758 758 .diffset-heading {
759 759 border: 1px solid @grey5;
760 760 margin-bottom: -1px;
761 761 // margin-top: 20px;
762 762 h2 {
763 763 margin: 0;
764 764 line-height: 38px;
765 765 padding-left: 10px;
766 766 }
767 767 .btn {
768 768 margin: 0;
769 769 }
770 770 background: @grey6;
771 771 display: block;
772 772 padding: 5px;
773 773 }
774 774 .diffset-heading-warning {
775 775 background: @alert3-inner;
776 776 border: 1px solid @alert3;
777 777 }
778 778 &.diffset-comments-disabled {
779 779 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
780 780 display: none !important;
781 781 }
782 782 }
783 783 }
784 784
785 785 .filelist {
786 786 .pill {
787 787 display: block;
788 788 float: left;
789 789 padding: @pill-padding-small;
790 790 }
791 791 }
792 792
793 793 .pill {
794 794 display: block;
795 795 float: left;
796 796 padding: @pill-padding;
797 797 }
798 798
799 799 .pill-group {
800 800 .pill {
801 801 opacity: .8;
802 802 margin-right: 3px;
803 803
804 804 &:first-child {
805 805 border-radius: @border-radius 0 0 @border-radius;
806 806 }
807 807 &:last-child {
808 808 border-radius: 0 @border-radius @border-radius 0;
809 809 }
810 810 &:only-child {
811 811 border-radius: @border-radius;
812 812 margin-right: 0;
813 813 }
814 814 }
815 815 }
816 816
817 817 /* Main comments*/
818 818 #comments {
819 819 .comment-selected {
820 820 border-left: 6px solid @comment-highlight-color;
821 821 padding-left: 3px;
822 822 margin-left: -9px;
823 823 }
824 824 }
825 825
826 826 .filediff {
827 827 border: 1px solid @grey5;
828 828
829 829 /* START OVERRIDES */
830 830 .code-highlight {
831 831 border: none; // TODO: remove this border from the global
832 832 // .code-highlight, it doesn't belong there
833 833 }
834 834 label {
835 835 margin: 0; // TODO: remove this margin definition from global label
836 836 // it doesn't belong there - if margin on labels
837 837 // are needed for a form they should be defined
838 838 // in the form's class
839 839 }
840 840 /* END OVERRIDES */
841 841
842 842 * {
843 843 box-sizing: border-box;
844 844 }
845 845 .filediff-anchor {
846 846 visibility: hidden;
847 847 }
848 848 &:hover {
849 849 .filediff-anchor {
850 850 visibility: visible;
851 851 }
852 852 }
853 853
854 854 .filediff-collapse-indicator {
855 855 border-style: solid;
856 856 float: left;
857 857 margin: 4px 0px 0 0;
858 858 cursor: pointer;
859 859 }
860 860
861 861 .filediff-heading {
862 862 background: @grey7;
863 863 cursor: pointer;
864 864 display: block;
865 865 padding: 5px 10px;
866 866 }
867 867 .filediff-heading:after {
868 868 content: "";
869 869 display: table;
870 870 clear: both;
871 871 }
872 872 .filediff-heading:hover {
873 873 background: #e1e9f4 !important;
874 874 }
875 875
876 876 .filediff-menu {
877 877 float: right;
878 878 text-align: right;
879 879 padding: 5px 5px 5px 0px;
880 880
881 881 &> a,
882 882 &> span {
883 883 padding: 1px;
884 884 }
885 885 }
886 886
887 887 .filediff-collapse-button, .filediff-expand-button {
888 888 cursor: pointer;
889 889 }
890 890 .filediff-collapse-button {
891 891 display: inline;
892 892 }
893 893 .filediff-expand-button {
894 894 display: none;
895 895 }
896 896 .filediff-collapsed .filediff-collapse-button {
897 897 display: none;
898 898 }
899 899 .filediff-collapsed .filediff-expand-button {
900 900 display: inline;
901 901 }
902 902
903 903 /**** COMMENTS ****/
904 904
905 905 .filediff-menu {
906 906 .show-comment-button {
907 907 display: none;
908 908 }
909 909 }
910 910 &.hide-comments {
911 911 .inline-comments {
912 912 display: none;
913 913 }
914 914 .filediff-menu {
915 915 .show-comment-button {
916 916 display: inline;
917 917 }
918 918 .hide-comment-button {
919 919 display: none;
920 920 }
921 921 }
922 922 }
923 923
924 924 .hide-line-comments {
925 925 .inline-comments {
926 926 display: none;
927 927 }
928 928 }
929 929
930 930 /**** END COMMENTS ****/
931 931
932 932 }
933 933
934 934
935 935
936 936 .filediff, .filelist {
937 937 .pill {
938 938 &[op="name"] {
939 939 background: none;
940 940 opacity: 1;
941 941 color: white;
942 942 }
943 943 &[op="limited"] {
944 944 background: @grey2;
945 945 color: white;
946 946 }
947 947 &[op="binary"] {
948 948 background: @color7;
949 949 color: white;
950 950 }
951 951 &[op="modified"] {
952 952 background: @alert1;
953 953 color: white;
954 954 }
955 955 &[op="renamed"] {
956 956 background: @color4;
957 957 color: white;
958 958 }
959 959 &[op="copied"] {
960 960 background: @color4;
961 961 color: white;
962 962 }
963 963 &[op="mode"] {
964 964 background: @grey3;
965 965 color: white;
966 966 }
967 967 &[op="symlink"] {
968 968 background: @color8;
969 969 color: white;
970 970 }
971 971
972 972 &[op="added"] { /* added lines */
973 973 background: @alert1;
974 974 color: white;
975 975 }
976 976 &[op="deleted"] { /* deleted lines */
977 977 background: @alert2;
978 978 color: white;
979 979 }
980 980
981 981 &[op="created"] { /* created file */
982 982 background: @alert1;
983 983 color: white;
984 984 }
985 985 &[op="removed"] { /* deleted file */
986 986 background: @color5;
987 987 color: white;
988 988 }
989 989 }
990 990 }
991 991
992 992
993 993 .filediff-outdated {
994 994 padding: 8px 0;
995 995
996 996 .filediff-heading {
997 997 opacity: .5;
998 998 }
999 999 }
1000 1000
1001 1001 table.cb {
1002 1002 width: 100%;
1003 1003 border-collapse: collapse;
1004 1004
1005 1005 .cb-text {
1006 1006 padding: @cb-text-padding;
1007 1007 }
1008 1008 .cb-hunk {
1009 1009 padding: @cb-text-padding;
1010 1010 }
1011 1011 .cb-expand {
1012 1012 display: none;
1013 1013 }
1014 1014 .cb-collapse {
1015 1015 display: inline;
1016 1016 }
1017 1017 &.cb-collapsed {
1018 1018 .cb-line {
1019 1019 display: none;
1020 1020 }
1021 1021 .cb-expand {
1022 1022 display: inline;
1023 1023 }
1024 1024 .cb-collapse {
1025 1025 display: none;
1026 1026 }
1027 1027 }
1028 1028
1029 1029 /* intentionally general selector since .cb-line-selected must override it
1030 1030 and they both use !important since the td itself may have a random color
1031 1031 generated by annotation blocks. TLDR: if you change it, make sure
1032 1032 annotated block selection and line selection in file view still work */
1033 1033 .cb-line-fresh .cb-content {
1034 1034 background: white !important;
1035 1035 }
1036 1036 .cb-warning {
1037 1037 background: #fff4dd;
1038 1038 }
1039 1039
1040 1040 &.cb-diff-sideside {
1041 1041 td {
1042 1042 &.cb-content {
1043 1043 width: 50%;
1044 1044 }
1045 1045 }
1046 1046 }
1047 1047
1048 1048 tr {
1049 1049 &.cb-annotate {
1050 1050 border-top: 1px solid #eee;
1051 1051 }
1052 1052
1053 1053 &.cb-comment-info {
1054 1054 border-top: 1px solid #eee;
1055 1055 color: rgba(0, 0, 0, 0.3);
1056 1056 background: #edf2f9;
1057 1057
1058 1058 td {
1059 1059
1060 1060 }
1061 1061 }
1062 1062
1063 1063 &.cb-hunk {
1064 1064 font-family: @font-family-monospace;
1065 1065 color: rgba(0, 0, 0, 0.3);
1066 1066
1067 1067 td {
1068 1068 &:first-child {
1069 1069 background: #edf2f9;
1070 1070 }
1071 1071 &:last-child {
1072 1072 background: #f4f7fb;
1073 1073 }
1074 1074 }
1075 1075 }
1076 1076 }
1077 1077
1078 1078
1079 1079 td {
1080 1080 vertical-align: top;
1081 1081 padding: 0;
1082 1082
1083 1083 &.cb-content {
1084 1084 font-size: 12.35px;
1085 1085
1086 1086 &.cb-line-selected .cb-code {
1087 1087 background: @comment-highlight-color !important;
1088 1088 }
1089 1089
1090 1090 span.cb-code {
1091 1091 line-height: @cb-line-height;
1092 1092 padding-left: @cb-line-code-padding;
1093 1093 padding-right: @cb-line-code-padding;
1094 1094 display: block;
1095 1095 white-space: pre-wrap;
1096 1096 font-family: @font-family-monospace;
1097 1097 word-break: break-all;
1098 1098 .nonl {
1099 1099 color: @color5;
1100 1100 }
1101 .cb-action {
1102 &:before {
1103 content: " ";
1104 }
1105 &.cb-deletion:before {
1106 content: "- ";
1107 }
1108 &.cb-addition:before {
1109 content: "+ ";
1110 }
1111 }
1101 1112 }
1102 1113
1103 1114 &> button.cb-comment-box-opener {
1104 1115
1105 1116 padding: 2px 2px 1px 3px;
1106 1117 margin-left: -6px;
1107 1118 margin-top: -1px;
1108 1119
1109 1120 border-radius: @border-radius;
1110 1121 position: absolute;
1111 1122 display: none;
1112 1123 }
1113 1124 .cb-comment {
1114 1125 margin-top: 10px;
1115 1126 white-space: normal;
1116 1127 }
1117 1128 }
1118 1129 &:hover {
1119 1130 button.cb-comment-box-opener {
1120 1131 display: block;
1121 1132 }
1122 1133 &+ td button.cb-comment-box-opener {
1123 1134 display: block
1124 1135 }
1125 1136 }
1126 1137
1127 1138 &.cb-data {
1128 1139 text-align: right;
1129 1140 width: 30px;
1130 1141 font-family: @font-family-monospace;
1131 1142
1132 1143 .icon-comment {
1133 1144 cursor: pointer;
1134 1145 }
1135 1146 &.cb-line-selected {
1136 1147 background: @comment-highlight-color !important;
1137 1148 }
1138 1149 &.cb-line-selected > div {
1139 1150 display: block;
1140 1151 background: @comment-highlight-color !important;
1141 1152 line-height: @cb-line-height;
1142 1153 color: rgba(0, 0, 0, 0.3);
1143 1154 }
1144 1155 }
1145 1156
1146 1157 &.cb-lineno {
1147 1158 padding: 0;
1148 1159 width: 50px;
1149 1160 color: rgba(0, 0, 0, 0.3);
1150 1161 text-align: right;
1151 1162 border-right: 1px solid #eee;
1152 1163 font-family: @font-family-monospace;
1153 1164 -webkit-user-select: none;
1154 1165 -moz-user-select: none;
1155 1166 user-select: none;
1156 1167
1157 1168 a::before {
1158 1169 content: attr(data-line-no);
1159 1170 }
1160 1171 &.cb-line-selected {
1161 1172 background: @comment-highlight-color !important;
1162 1173 }
1163 1174
1164 1175 a {
1165 1176 display: block;
1166 1177 padding-right: @cb-line-code-padding;
1167 1178 padding-left: @cb-line-code-padding;
1168 1179 line-height: @cb-line-height;
1169 1180 color: rgba(0, 0, 0, 0.3);
1170 1181 }
1171 1182 }
1172 1183
1173 1184 &.cb-empty {
1174 1185 background: @grey7;
1175 1186 }
1176 1187
1177 1188 ins {
1178 1189 color: black;
1179 1190 background: #a6f3a6;
1180 1191 text-decoration: none;
1181 1192 }
1182 1193 del {
1183 1194 color: black;
1184 1195 background: #f8cbcb;
1185 1196 text-decoration: none;
1186 1197 }
1187 1198 &.cb-addition {
1188 1199 background: #ecffec;
1189 1200
1190 1201 &.blob-lineno {
1191 1202 background: #ddffdd;
1192 1203 }
1193 1204 }
1194 1205 &.cb-deletion {
1195 1206 background: #ffecec;
1196 1207
1197 1208 &.blob-lineno {
1198 1209 background: #ffdddd;
1199 1210 }
1200 1211 }
1201 1212 &.cb-annotate-message-spacer {
1202 1213 width:8px;
1203 1214 padding: 1px 0px 0px 3px;
1204 1215 }
1205 1216 &.cb-annotate-info {
1206 1217 width: 320px;
1207 1218 min-width: 320px;
1208 1219 max-width: 320px;
1209 1220 padding: 5px 2px;
1210 1221 font-size: 13px;
1211 1222
1212 1223 .cb-annotate-message {
1213 1224 padding: 2px 0px 0px 0px;
1214 1225 white-space: pre-line;
1215 1226 overflow: hidden;
1216 1227 }
1217 1228 .rc-user {
1218 1229 float: none;
1219 1230 padding: 0 6px 0 17px;
1220 1231 min-width: unset;
1221 1232 min-height: unset;
1222 1233 }
1223 1234 }
1224 1235
1225 1236 &.cb-annotate-revision {
1226 1237 cursor: pointer;
1227 1238 text-align: right;
1228 1239 padding: 1px 3px 0px 3px;
1229 1240 }
1230 1241 }
1231 1242 }
@@ -1,1016 +1,1016 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 446 %if filediff.diffset.source_ref:
447 447 %if filediff.operation in ['D', 'M']:
448 448 <a
449 449 class="tooltip"
450 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)}"
451 451 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
452 452 >
453 453 ${_('Show file before')}
454 454 </a> |
455 455 %else:
456 456 <span
457 457 class="tooltip"
458 458 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
459 459 >
460 460 ${_('Show file before')}
461 461 </span> |
462 462 %endif
463 463 %if filediff.operation in ['A', 'M']:
464 464 <a
465 465 class="tooltip"
466 466 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 467 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
468 468 >
469 469 ${_('Show file after')}
470 470 </a>
471 471 %else:
472 472 <span
473 473 class="tooltip"
474 474 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
475 475 >
476 476 ${_('Show file after')}
477 477 </span>
478 478 %endif
479 479
480 480 % if use_comments:
481 481 |
482 482 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
483 483 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
484 484 </a>
485 485 % endif
486 486
487 487 %endif
488 488 </div>
489 489 </%def>
490 490
491 491
492 492 <%def name="inline_comments_container(comments, inline_comments)">
493 493 <div class="inline-comments">
494 494 %for comment in comments:
495 495 ${commentblock.comment_block(comment, inline=True)}
496 496 %endfor
497 497 % if comments and comments[-1].outdated:
498 498 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
499 499 style="display: none;}">
500 500 ${_('Add another comment')}
501 501 </span>
502 502 % else:
503 503 <span onclick="return Rhodecode.comments.createComment(this)"
504 504 class="btn btn-secondary cb-comment-add-button">
505 505 ${_('Add another comment')}
506 506 </span>
507 507 % endif
508 508
509 509 </div>
510 510 </%def>
511 511
512 512 <%!
513 513 def get_comments_for(diff_type, comments, filename, line_version, line_number):
514 514 if hasattr(filename, 'unicode_path'):
515 515 filename = filename.unicode_path
516 516
517 517 if not isinstance(filename, basestring):
518 518 return None
519 519
520 520 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
521 521
522 522 if comments and filename in comments:
523 523 file_comments = comments[filename]
524 524 if line_key in file_comments:
525 525 data = file_comments.pop(line_key)
526 526 return data
527 527 %>
528 528
529 529 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
530 530 %for i, line in enumerate(hunk.sideside):
531 531 <%
532 532 old_line_anchor, new_line_anchor = None, None
533 533
534 534 if line.original.lineno:
535 535 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
536 536 if line.modified.lineno:
537 537 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
538 538 %>
539 539
540 540 <tr class="cb-line">
541 541 <td class="cb-data ${action_class(line.original.action)}"
542 542 data-line-no="${line.original.lineno}"
543 543 >
544 544 <div>
545 545
546 546 <% line_old_comments = None %>
547 547 %if line.original.get_comment_args:
548 548 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
549 549 %endif
550 550 %if line_old_comments:
551 551 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
552 552 % if has_outdated:
553 553 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
554 554 % else:
555 555 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
556 556 % endif
557 557 %endif
558 558 </div>
559 559 </td>
560 560 <td class="cb-lineno ${action_class(line.original.action)}"
561 561 data-line-no="${line.original.lineno}"
562 562 %if old_line_anchor:
563 563 id="${old_line_anchor}"
564 564 %endif
565 565 >
566 566 %if line.original.lineno:
567 567 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
568 568 %endif
569 569 </td>
570 570 <td class="cb-content ${action_class(line.original.action)}"
571 571 data-line-no="o${line.original.lineno}"
572 572 >
573 573 %if use_comments and line.original.lineno:
574 574 ${render_add_comment_button()}
575 575 %endif
576 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
576 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
577 577
578 578 %if use_comments and line.original.lineno and line_old_comments:
579 579 ${inline_comments_container(line_old_comments, inline_comments)}
580 580 %endif
581 581
582 582 </td>
583 583 <td class="cb-data ${action_class(line.modified.action)}"
584 584 data-line-no="${line.modified.lineno}"
585 585 >
586 586 <div>
587 587
588 588 %if line.modified.get_comment_args:
589 589 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
590 590 %else:
591 591 <% line_new_comments = None%>
592 592 %endif
593 593 %if line_new_comments:
594 594 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
595 595 % if has_outdated:
596 596 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
597 597 % else:
598 598 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
599 599 % endif
600 600 %endif
601 601 </div>
602 602 </td>
603 603 <td class="cb-lineno ${action_class(line.modified.action)}"
604 604 data-line-no="${line.modified.lineno}"
605 605 %if new_line_anchor:
606 606 id="${new_line_anchor}"
607 607 %endif
608 608 >
609 609 %if line.modified.lineno:
610 610 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
611 611 %endif
612 612 </td>
613 613 <td class="cb-content ${action_class(line.modified.action)}"
614 614 data-line-no="n${line.modified.lineno}"
615 615 >
616 616 %if use_comments and line.modified.lineno:
617 617 ${render_add_comment_button()}
618 618 %endif
619 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
619 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
620 620 %if use_comments and line.modified.lineno and line_new_comments:
621 621 ${inline_comments_container(line_new_comments, inline_comments)}
622 622 %endif
623 623 </td>
624 624 </tr>
625 625 %endfor
626 626 </%def>
627 627
628 628
629 629 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
630 630 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
631 631
632 632 <%
633 633 old_line_anchor, new_line_anchor = None, None
634 634 if old_line_no:
635 635 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
636 636 if new_line_no:
637 637 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
638 638 %>
639 639 <tr class="cb-line">
640 640 <td class="cb-data ${action_class(action)}">
641 641 <div>
642 642
643 643 %if comments_args:
644 644 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
645 645 %else:
646 646 <% comments = None %>
647 647 %endif
648 648
649 649 % if comments:
650 650 <% has_outdated = any([x.outdated for x in comments]) %>
651 651 % if has_outdated:
652 652 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
653 653 % else:
654 654 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
655 655 % endif
656 656 % endif
657 657 </div>
658 658 </td>
659 659 <td class="cb-lineno ${action_class(action)}"
660 660 data-line-no="${old_line_no}"
661 661 %if old_line_anchor:
662 662 id="${old_line_anchor}"
663 663 %endif
664 664 >
665 665 %if old_line_anchor:
666 666 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
667 667 %endif
668 668 </td>
669 669 <td class="cb-lineno ${action_class(action)}"
670 670 data-line-no="${new_line_no}"
671 671 %if new_line_anchor:
672 672 id="${new_line_anchor}"
673 673 %endif
674 674 >
675 675 %if new_line_anchor:
676 676 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
677 677 %endif
678 678 </td>
679 679 <td class="cb-content ${action_class(action)}"
680 680 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
681 681 >
682 682 %if use_comments:
683 683 ${render_add_comment_button()}
684 684 %endif
685 <span class="cb-code">${action} ${content or '' | n}</span>
685 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
686 686 %if use_comments and comments:
687 687 ${inline_comments_container(comments, inline_comments)}
688 688 %endif
689 689 </td>
690 690 </tr>
691 691 %endfor
692 692 </%def>
693 693
694 694
695 695 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
696 696 % if diff_mode == 'unified':
697 697 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
698 698 % elif diff_mode == 'sideside':
699 699 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
700 700 % else:
701 701 <tr class="cb-line">
702 702 <td>unknown diff mode</td>
703 703 </tr>
704 704 % endif
705 705 </%def>file changes
706 706
707 707
708 708 <%def name="render_add_comment_button()">
709 709 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
710 710 <span><i class="icon-comment"></i></span>
711 711 </button>
712 712 </%def>
713 713
714 714 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
715 715
716 716 <div id="diff-file-sticky" class="diffset-menu clearinner">
717 717 ## auto adjustable
718 718 <div class="sidebar__inner">
719 719 <div class="sidebar__bar">
720 720 <div class="pull-right">
721 721 <div class="btn-group">
722 722
723 723 ## DIFF OPTIONS via Select2
724 724 <div class="pull-left">
725 725 ${h.hidden('diff_menu')}
726 726 </div>
727 727
728 728 <a
729 729 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
730 730 title="${h.tooltip(_('View side by side'))}"
731 731 href="${h.current_route_path(request, diffmode='sideside')}">
732 732 <span>${_('Side by Side')}</span>
733 733 </a>
734 734
735 735 <a
736 736 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
737 737 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
738 738 <span>${_('Unified')}</span>
739 739 </a>
740 740
741 741 % if range_diff_on is True:
742 742 <a
743 743 title="${_('Turn off: Show the diff as commit range')}"
744 744 class="btn btn-primary"
745 745 href="${h.current_route_path(request, **{"range-diff":"0"})}">
746 746 <span>${_('Range Diff')}</span>
747 747 </a>
748 748 % elif range_diff_on is False:
749 749 <a
750 750 title="${_('Show the diff as commit range')}"
751 751 class="btn"
752 752 href="${h.current_route_path(request, **{"range-diff":"1"})}">
753 753 <span>${_('Range Diff')}</span>
754 754 </a>
755 755 % endif
756 756 </div>
757 757 </div>
758 758 <div class="pull-left">
759 759 <div class="btn-group">
760 760 <div class="pull-left">
761 761 ${h.hidden('file_filter')}
762 762 </div>
763 763 <a
764 764 class="btn"
765 765 href="#"
766 766 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
767 767 <a
768 768 class="btn"
769 769 href="#"
770 770 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
771 771 </div>
772 772 </div>
773 773 </div>
774 774 <div class="fpath-placeholder">
775 775 <i class="icon-file-text"></i>
776 776 <strong class="fpath-placeholder-text">
777 777 Context file:
778 778 </strong>
779 779 </div>
780 780 <div class="sidebar_inner_shadow"></div>
781 781 </div>
782 782 </div>
783 783
784 784 % if diffset:
785 785
786 786 %if diffset.limited_diff:
787 787 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
788 788 %else:
789 789 <% 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 790 %endif
791 791 ## case on range-diff placeholder needs to be updated
792 792 % if range_diff_on is True:
793 793 <% file_placeholder = _('Disabled on range diff') %>
794 794 % endif
795 795
796 796 <script>
797 797
798 798 var feedFilesOptions = function (query, initialData) {
799 799 var data = {results: []};
800 800 var isQuery = typeof query.term !== 'undefined';
801 801
802 802 var section = _gettext('Changed files');
803 803 var filteredData = [];
804 804
805 805 //filter results
806 806 $.each(initialData.results, function (idx, value) {
807 807
808 808 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
809 809 filteredData.push({
810 810 'id': this.id,
811 811 'text': this.text,
812 812 "ops": this.ops,
813 813 })
814 814 }
815 815
816 816 });
817 817
818 818 data.results = filteredData;
819 819
820 820 query.callback(data);
821 821 };
822 822
823 823 var formatFileResult = function(result, container, query, escapeMarkup) {
824 824 return function(data, escapeMarkup) {
825 825 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
826 826 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
827 827 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
828 828 '<span class="pill" op="added">{0}</span>' +
829 829 '<span class="pill" op="deleted">{1}</span>' +
830 830 '</span>'
831 831 ;
832 832 var added = data['ops']['added'];
833 833 if (added === 0) {
834 834 // don't show +0
835 835 added = 0;
836 836 } else {
837 837 added = '+' + added;
838 838 }
839 839
840 840 var deleted = -1*data['ops']['deleted'];
841 841
842 842 tmpl += pill.format(added, deleted);
843 843 return container.format(tmpl);
844 844
845 845 }(result, escapeMarkup);
846 846 };
847 847 var preloadData = {
848 848 results: [
849 849 % for filediff in diffset.files:
850 850 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
851 851 text:"${filediff.patch['filename']}",
852 852 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
853 853 % endfor
854 854 ]
855 855 };
856 856
857 857 $(document).ready(function () {
858 858
859 859 var fileFilter = $("#file_filter").select2({
860 860 'dropdownAutoWidth': true,
861 861 'width': 'auto',
862 862 'placeholder': "${file_placeholder}",
863 863 containerCssClass: "drop-menu",
864 864 dropdownCssClass: "drop-menu-dropdown",
865 865 data: preloadData,
866 866 query: function(query) {
867 867 feedFilesOptions(query, preloadData);
868 868 },
869 869 formatResult: formatFileResult
870 870 });
871 871 % if range_diff_on is True:
872 872 fileFilter.select2("enable", false);
873 873
874 874 % endif
875 875
876 876 $("#file_filter").on('click', function (e) {
877 877 e.preventDefault();
878 878 var selected = $('#file_filter').select2('data');
879 879 var idSelector = "#"+selected.id;
880 880 window.location.hash = idSelector;
881 881 // expand the container if we quick-select the field
882 882 $(idSelector).next().prop('checked', false);
883 883 updateSticky()
884 884 });
885 885
886 886 var contextPrefix = _gettext('Context file: ');
887 887 ## sticky sidebar
888 888 var sidebarElement = document.getElementById('diff-file-sticky');
889 889 sidebar = new StickySidebar(sidebarElement, {
890 890 topSpacing: 0,
891 891 bottomSpacing: 0,
892 892 innerWrapperSelector: '.sidebar__inner'
893 893 });
894 894 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
895 895 // reset our file so it's not holding new value
896 896 $('.fpath-placeholder-text').html(contextPrefix)
897 897 });
898 898
899 899 updateSticky = function () {
900 900 sidebar.updateSticky();
901 901 Waypoint.refreshAll();
902 902 };
903 903
904 904 var animateText = $.debounce(100, function(fPath, anchorId) {
905 905 // animate setting the text
906 906 var callback = function () {
907 907 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
908 908 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
909 909 };
910 910 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
911 911 });
912 912
913 913 ## dynamic file waypoints
914 914 var setFPathInfo = function(fPath, anchorId){
915 915 animateText(fPath, anchorId)
916 916 };
917 917
918 918 var codeBlock = $('.filediff');
919 919 // forward waypoint
920 920 codeBlock.waypoint(
921 921 function(direction) {
922 922 if (direction === "down"){
923 923 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
924 924 }
925 925 }, {
926 926 offset: 70,
927 927 context: '.fpath-placeholder'
928 928 }
929 929 );
930 930
931 931 // backward waypoint
932 932 codeBlock.waypoint(
933 933 function(direction) {
934 934 if (direction === "up"){
935 935 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
936 936 }
937 937 }, {
938 938 offset: function () {
939 939 return -this.element.clientHeight + 90
940 940 },
941 941 context: '.fpath-placeholder'
942 942 }
943 943 );
944 944
945 945 var preloadData = {
946 946 results: [
947 947 ## Wide diff mode
948 948 {
949 949 id: 1,
950 text: _gettext('Toggle Wide Mode Diff'),
950 text: _gettext('Toggle Wide Mode diff'),
951 951 action: function () {
952 952 updateSticky();
953 953 Rhodecode.comments.toggleWideMode(this);
954 954 return null;
955 955 },
956 956 url: null,
957 957 },
958 958
959 959 ## Whitespace change
960 960 % if request.GET.get('ignorews', '') == '1':
961 961 {
962 962 id: 2,
963 963 text: _gettext('Show whitespace changes'),
964 964 action: function () {},
965 965 url: "${h.current_route_path(request, ignorews=0)|n}"
966 966 },
967 967 % else:
968 968 {
969 969 id: 2,
970 970 text: _gettext('Hide whitespace changes'),
971 971 action: function () {},
972 972 url: "${h.current_route_path(request, ignorews=1)|n}"
973 973 },
974 974 % endif
975 975
976 976 ## FULL CONTEXT
977 977 % if request.GET.get('fullcontext', '') == '1':
978 978 {
979 979 id: 3,
980 980 text: _gettext('Hide full context diff'),
981 981 action: function () {},
982 982 url: "${h.current_route_path(request, fullcontext=0)|n}"
983 983 },
984 984 % else:
985 985 {
986 986 id: 3,
987 987 text: _gettext('Show full context diff'),
988 988 action: function () {},
989 989 url: "${h.current_route_path(request, fullcontext=1)|n}"
990 990 },
991 991 % endif
992 992
993 993 ]
994 994 };
995 995
996 996 $("#diff_menu").select2({
997 997 minimumResultsForSearch: -1,
998 998 containerCssClass: "drop-menu",
999 999 dropdownCssClass: "drop-menu-dropdown",
1000 1000 dropdownAutoWidth: true,
1001 1001 data: preloadData,
1002 1002 placeholder: "${_('Diff Options')}",
1003 1003 });
1004 1004 $("#diff_menu").on('select2-selecting', function (e) {
1005 1005 e.choice.action();
1006 1006 if (e.choice.url !== null) {
1007 1007 window.location = e.choice.url
1008 1008 }
1009 1009 });
1010 1010
1011 1011 });
1012 1012
1013 1013 </script>
1014 1014 % endif
1015 1015
1016 </%def> No newline at end of file
1016 </%def>
General Comments 0
You need to be logged in to leave comments. Login now