##// END OF EJS Templates
ux: better tooltip for inline comment toggle
lisaq -
r881:aafa0b10 default
parent child Browse files
Show More
@@ -1,886 +1,886 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Set of diffing helpers, previously part of vcs
23 Set of diffing helpers, previously part of vcs
24 """
24 """
25
25
26 import collections
26 import collections
27 import re
27 import re
28 import difflib
28 import difflib
29 import logging
29 import logging
30
30
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34
34
35 from rhodecode.lib.vcs.exceptions import VCSError
35 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 from rhodecode.lib.helpers import escape
38 from rhodecode.lib.helpers import escape
39 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43 # define max context, a file with more than this numbers of lines is unusable
43 # define max context, a file with more than this numbers of lines is unusable
44 # in browser anyway
44 # in browser anyway
45 MAX_CONTEXT = 1024 * 1014
45 MAX_CONTEXT = 1024 * 1014
46
46
47
47
48 class OPS(object):
48 class OPS(object):
49 ADD = 'A'
49 ADD = 'A'
50 MOD = 'M'
50 MOD = 'M'
51 DEL = 'D'
51 DEL = 'D'
52
52
53
53
54 def wrap_to_table(str_):
54 def wrap_to_table(str_):
55 return '''<table class="code-difftable">
55 return '''<table class="code-difftable">
56 <tr class="line no-comment">
56 <tr class="line no-comment">
57 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
57 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
58 <td></td>
58 <td></td>
59 <td class="lineno new"></td>
59 <td class="lineno new"></td>
60 <td class="code no-comment"><pre>%s</pre></td>
60 <td class="code no-comment"><pre>%s</pre></td>
61 </tr>
61 </tr>
62 </table>''' % (_('Click to comment'), str_)
62 </table>''' % (_('Click to comment'), str_)
63
63
64
64
65 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
65 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
66 show_full_diff=False, ignore_whitespace=True, line_context=3,
66 show_full_diff=False, ignore_whitespace=True, line_context=3,
67 enable_comments=False):
67 enable_comments=False):
68 """
68 """
69 returns a wrapped diff into a table, checks for cut_off_limit for file and
69 returns a wrapped diff into a table, checks for cut_off_limit for file and
70 whole diff and presents proper message
70 whole diff and presents proper message
71 """
71 """
72
72
73 if filenode_old is None:
73 if filenode_old is None:
74 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
74 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
75
75
76 if filenode_old.is_binary or filenode_new.is_binary:
76 if filenode_old.is_binary or filenode_new.is_binary:
77 diff = wrap_to_table(_('Binary file'))
77 diff = wrap_to_table(_('Binary file'))
78 stats = None
78 stats = None
79 size = 0
79 size = 0
80 data = None
80 data = None
81
81
82 elif diff_limit != -1 and (diff_limit is None or
82 elif diff_limit != -1 and (diff_limit is None or
83 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
83 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
84
84
85 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
85 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
86 ignore_whitespace=ignore_whitespace,
86 ignore_whitespace=ignore_whitespace,
87 context=line_context)
87 context=line_context)
88 diff_processor = DiffProcessor(
88 diff_processor = DiffProcessor(
89 f_gitdiff, format='gitdiff', diff_limit=diff_limit,
89 f_gitdiff, format='gitdiff', diff_limit=diff_limit,
90 file_limit=file_limit, show_full_diff=show_full_diff)
90 file_limit=file_limit, show_full_diff=show_full_diff)
91 _parsed = diff_processor.prepare()
91 _parsed = diff_processor.prepare()
92
92
93 diff = diff_processor.as_html(enable_comments=enable_comments)
93 diff = diff_processor.as_html(enable_comments=enable_comments)
94 stats = _parsed[0]['stats'] if _parsed else None
94 stats = _parsed[0]['stats'] if _parsed else None
95 size = len(diff or '')
95 size = len(diff or '')
96 data = _parsed[0] if _parsed else None
96 data = _parsed[0] if _parsed else None
97 else:
97 else:
98 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
98 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
99 'diff menu to display this diff'))
99 'diff menu to display this diff'))
100 stats = None
100 stats = None
101 size = 0
101 size = 0
102 data = None
102 data = None
103 if not diff:
103 if not diff:
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 [filenode_new, filenode_old])
105 [filenode_new, filenode_old])
106 if submodules:
106 if submodules:
107 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
107 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
108 else:
108 else:
109 diff = wrap_to_table(_('No changes detected'))
109 diff = wrap_to_table(_('No changes detected'))
110
110
111 cs1 = filenode_old.commit.raw_id
111 cs1 = filenode_old.commit.raw_id
112 cs2 = filenode_new.commit.raw_id
112 cs2 = filenode_new.commit.raw_id
113
113
114 return size, cs1, cs2, diff, stats, data
114 return size, cs1, cs2, diff, stats, data
115
115
116
116
117 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
117 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
118 """
118 """
119 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
119 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
120
120
121 :param ignore_whitespace: ignore whitespaces in diff
121 :param ignore_whitespace: ignore whitespaces in diff
122 """
122 """
123 # make sure we pass in default context
123 # make sure we pass in default context
124 context = context or 3
124 context = context or 3
125 # protect against IntOverflow when passing HUGE context
125 # protect against IntOverflow when passing HUGE context
126 if context > MAX_CONTEXT:
126 if context > MAX_CONTEXT:
127 context = MAX_CONTEXT
127 context = MAX_CONTEXT
128
128
129 submodules = filter(lambda o: isinstance(o, SubModuleNode),
129 submodules = filter(lambda o: isinstance(o, SubModuleNode),
130 [filenode_new, filenode_old])
130 [filenode_new, filenode_old])
131 if submodules:
131 if submodules:
132 return ''
132 return ''
133
133
134 for filenode in (filenode_old, filenode_new):
134 for filenode in (filenode_old, filenode_new):
135 if not isinstance(filenode, FileNode):
135 if not isinstance(filenode, FileNode):
136 raise VCSError(
136 raise VCSError(
137 "Given object should be FileNode object, not %s"
137 "Given object should be FileNode object, not %s"
138 % filenode.__class__)
138 % filenode.__class__)
139
139
140 repo = filenode_new.commit.repository
140 repo = filenode_new.commit.repository
141 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
141 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
142 new_commit = filenode_new.commit
142 new_commit = filenode_new.commit
143
143
144 vcs_gitdiff = repo.get_diff(
144 vcs_gitdiff = repo.get_diff(
145 old_commit, new_commit, filenode_new.path,
145 old_commit, new_commit, filenode_new.path,
146 ignore_whitespace, context, path1=filenode_old.path)
146 ignore_whitespace, context, path1=filenode_old.path)
147 return vcs_gitdiff
147 return vcs_gitdiff
148
148
149 NEW_FILENODE = 1
149 NEW_FILENODE = 1
150 DEL_FILENODE = 2
150 DEL_FILENODE = 2
151 MOD_FILENODE = 3
151 MOD_FILENODE = 3
152 RENAMED_FILENODE = 4
152 RENAMED_FILENODE = 4
153 COPIED_FILENODE = 5
153 COPIED_FILENODE = 5
154 CHMOD_FILENODE = 6
154 CHMOD_FILENODE = 6
155 BIN_FILENODE = 7
155 BIN_FILENODE = 7
156
156
157
157
158 class LimitedDiffContainer(object):
158 class LimitedDiffContainer(object):
159
159
160 def __init__(self, diff_limit, cur_diff_size, diff):
160 def __init__(self, diff_limit, cur_diff_size, diff):
161 self.diff = diff
161 self.diff = diff
162 self.diff_limit = diff_limit
162 self.diff_limit = diff_limit
163 self.cur_diff_size = cur_diff_size
163 self.cur_diff_size = cur_diff_size
164
164
165 def __getitem__(self, key):
165 def __getitem__(self, key):
166 return self.diff.__getitem__(key)
166 return self.diff.__getitem__(key)
167
167
168 def __iter__(self):
168 def __iter__(self):
169 for l in self.diff:
169 for l in self.diff:
170 yield l
170 yield l
171
171
172
172
173 class Action(object):
173 class Action(object):
174 """
174 """
175 Contains constants for the action value of the lines in a parsed diff.
175 Contains constants for the action value of the lines in a parsed diff.
176 """
176 """
177
177
178 ADD = 'add'
178 ADD = 'add'
179 DELETE = 'del'
179 DELETE = 'del'
180 UNMODIFIED = 'unmod'
180 UNMODIFIED = 'unmod'
181
181
182 CONTEXT = 'context'
182 CONTEXT = 'context'
183
183
184
184
185 class DiffProcessor(object):
185 class DiffProcessor(object):
186 """
186 """
187 Give it a unified or git diff and it returns a list of the files that were
187 Give it a unified or git diff and it returns a list of the files that were
188 mentioned in the diff together with a dict of meta information that
188 mentioned in the diff together with a dict of meta information that
189 can be used to render it in a HTML template.
189 can be used to render it in a HTML template.
190
190
191 .. note:: Unicode handling
191 .. note:: Unicode handling
192
192
193 The original diffs are a byte sequence and can contain filenames
193 The original diffs are a byte sequence and can contain filenames
194 in mixed encodings. This class generally returns `unicode` objects
194 in mixed encodings. This class generally returns `unicode` objects
195 since the result is intended for presentation to the user.
195 since the result is intended for presentation to the user.
196
196
197 """
197 """
198 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
198 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
199 _newline_marker = re.compile(r'^\\ No newline at end of file')
199 _newline_marker = re.compile(r'^\\ No newline at end of file')
200
200
201 # used for inline highlighter word split
201 # used for inline highlighter word split
202 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
202 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
203
203
204 def __init__(self, diff, format='gitdiff', diff_limit=None,
204 def __init__(self, diff, format='gitdiff', diff_limit=None,
205 file_limit=None, show_full_diff=True):
205 file_limit=None, show_full_diff=True):
206 """
206 """
207 :param diff: A `Diff` object representing a diff from a vcs backend
207 :param diff: A `Diff` object representing a diff from a vcs backend
208 :param format: format of diff passed, `udiff` or `gitdiff`
208 :param format: format of diff passed, `udiff` or `gitdiff`
209 :param diff_limit: define the size of diff that is considered "big"
209 :param diff_limit: define the size of diff that is considered "big"
210 based on that parameter cut off will be triggered, set to None
210 based on that parameter cut off will be triggered, set to None
211 to show full diff
211 to show full diff
212 """
212 """
213 self._diff = diff
213 self._diff = diff
214 self._format = format
214 self._format = format
215 self.adds = 0
215 self.adds = 0
216 self.removes = 0
216 self.removes = 0
217 # calculate diff size
217 # calculate diff size
218 self.diff_limit = diff_limit
218 self.diff_limit = diff_limit
219 self.file_limit = file_limit
219 self.file_limit = file_limit
220 self.show_full_diff = show_full_diff
220 self.show_full_diff = show_full_diff
221 self.cur_diff_size = 0
221 self.cur_diff_size = 0
222 self.parsed = False
222 self.parsed = False
223 self.parsed_diff = []
223 self.parsed_diff = []
224
224
225 if format == 'gitdiff':
225 if format == 'gitdiff':
226 self.differ = self._highlight_line_difflib
226 self.differ = self._highlight_line_difflib
227 self._parser = self._parse_gitdiff
227 self._parser = self._parse_gitdiff
228 else:
228 else:
229 self.differ = self._highlight_line_udiff
229 self.differ = self._highlight_line_udiff
230 self._parser = self._parse_udiff
230 self._parser = self._parse_udiff
231
231
232 def _copy_iterator(self):
232 def _copy_iterator(self):
233 """
233 """
234 make a fresh copy of generator, we should not iterate thru
234 make a fresh copy of generator, we should not iterate thru
235 an original as it's needed for repeating operations on
235 an original as it's needed for repeating operations on
236 this instance of DiffProcessor
236 this instance of DiffProcessor
237 """
237 """
238 self.__udiff, iterator_copy = tee(self.__udiff)
238 self.__udiff, iterator_copy = tee(self.__udiff)
239 return iterator_copy
239 return iterator_copy
240
240
241 def _escaper(self, string):
241 def _escaper(self, string):
242 """
242 """
243 Escaper for diff escapes special chars and checks the diff limit
243 Escaper for diff escapes special chars and checks the diff limit
244
244
245 :param string:
245 :param string:
246 """
246 """
247
247
248 self.cur_diff_size += len(string)
248 self.cur_diff_size += len(string)
249
249
250 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
250 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
251 raise DiffLimitExceeded('Diff Limit Exceeded')
251 raise DiffLimitExceeded('Diff Limit Exceeded')
252
252
253 return safe_unicode(string)\
253 return safe_unicode(string)\
254 .replace('&', '&amp;')\
254 .replace('&', '&amp;')\
255 .replace('<', '&lt;')\
255 .replace('<', '&lt;')\
256 .replace('>', '&gt;')
256 .replace('>', '&gt;')
257
257
258 def _line_counter(self, l):
258 def _line_counter(self, l):
259 """
259 """
260 Checks each line and bumps total adds/removes for this diff
260 Checks each line and bumps total adds/removes for this diff
261
261
262 :param l:
262 :param l:
263 """
263 """
264 if l.startswith('+') and not l.startswith('+++'):
264 if l.startswith('+') and not l.startswith('+++'):
265 self.adds += 1
265 self.adds += 1
266 elif l.startswith('-') and not l.startswith('---'):
266 elif l.startswith('-') and not l.startswith('---'):
267 self.removes += 1
267 self.removes += 1
268 return safe_unicode(l)
268 return safe_unicode(l)
269
269
270 def _highlight_line_difflib(self, line, next_):
270 def _highlight_line_difflib(self, line, next_):
271 """
271 """
272 Highlight inline changes in both lines.
272 Highlight inline changes in both lines.
273 """
273 """
274
274
275 if line['action'] == Action.DELETE:
275 if line['action'] == Action.DELETE:
276 old, new = line, next_
276 old, new = line, next_
277 else:
277 else:
278 old, new = next_, line
278 old, new = next_, line
279
279
280 oldwords = self._token_re.split(old['line'])
280 oldwords = self._token_re.split(old['line'])
281 newwords = self._token_re.split(new['line'])
281 newwords = self._token_re.split(new['line'])
282 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
282 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
283
283
284 oldfragments, newfragments = [], []
284 oldfragments, newfragments = [], []
285 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
285 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
286 oldfrag = ''.join(oldwords[i1:i2])
286 oldfrag = ''.join(oldwords[i1:i2])
287 newfrag = ''.join(newwords[j1:j2])
287 newfrag = ''.join(newwords[j1:j2])
288 if tag != 'equal':
288 if tag != 'equal':
289 if oldfrag:
289 if oldfrag:
290 oldfrag = '<del>%s</del>' % oldfrag
290 oldfrag = '<del>%s</del>' % oldfrag
291 if newfrag:
291 if newfrag:
292 newfrag = '<ins>%s</ins>' % newfrag
292 newfrag = '<ins>%s</ins>' % newfrag
293 oldfragments.append(oldfrag)
293 oldfragments.append(oldfrag)
294 newfragments.append(newfrag)
294 newfragments.append(newfrag)
295
295
296 old['line'] = "".join(oldfragments)
296 old['line'] = "".join(oldfragments)
297 new['line'] = "".join(newfragments)
297 new['line'] = "".join(newfragments)
298
298
299 def _highlight_line_udiff(self, line, next_):
299 def _highlight_line_udiff(self, line, next_):
300 """
300 """
301 Highlight inline changes in both lines.
301 Highlight inline changes in both lines.
302 """
302 """
303 start = 0
303 start = 0
304 limit = min(len(line['line']), len(next_['line']))
304 limit = min(len(line['line']), len(next_['line']))
305 while start < limit and line['line'][start] == next_['line'][start]:
305 while start < limit and line['line'][start] == next_['line'][start]:
306 start += 1
306 start += 1
307 end = -1
307 end = -1
308 limit -= start
308 limit -= start
309 while -end <= limit and line['line'][end] == next_['line'][end]:
309 while -end <= limit and line['line'][end] == next_['line'][end]:
310 end -= 1
310 end -= 1
311 end += 1
311 end += 1
312 if start or end:
312 if start or end:
313 def do(l):
313 def do(l):
314 last = end + len(l['line'])
314 last = end + len(l['line'])
315 if l['action'] == Action.ADD:
315 if l['action'] == Action.ADD:
316 tag = 'ins'
316 tag = 'ins'
317 else:
317 else:
318 tag = 'del'
318 tag = 'del'
319 l['line'] = '%s<%s>%s</%s>%s' % (
319 l['line'] = '%s<%s>%s</%s>%s' % (
320 l['line'][:start],
320 l['line'][:start],
321 tag,
321 tag,
322 l['line'][start:last],
322 l['line'][start:last],
323 tag,
323 tag,
324 l['line'][last:]
324 l['line'][last:]
325 )
325 )
326 do(line)
326 do(line)
327 do(next_)
327 do(next_)
328
328
329 def _clean_line(self, line, command):
329 def _clean_line(self, line, command):
330 if command in ['+', '-', ' ']:
330 if command in ['+', '-', ' ']:
331 # only modify the line if it's actually a diff thing
331 # only modify the line if it's actually a diff thing
332 line = line[1:]
332 line = line[1:]
333 return line
333 return line
334
334
335 def _parse_gitdiff(self, inline_diff=True):
335 def _parse_gitdiff(self, inline_diff=True):
336 _files = []
336 _files = []
337 diff_container = lambda arg: arg
337 diff_container = lambda arg: arg
338
338
339 for chunk in self._diff.chunks():
339 for chunk in self._diff.chunks():
340 head = chunk.header
340 head = chunk.header
341
341
342 diff = imap(self._escaper, chunk.diff.splitlines(1))
342 diff = imap(self._escaper, chunk.diff.splitlines(1))
343 raw_diff = chunk.raw
343 raw_diff = chunk.raw
344 limited_diff = False
344 limited_diff = False
345 exceeds_limit = False
345 exceeds_limit = False
346
346
347 op = None
347 op = None
348 stats = {
348 stats = {
349 'added': 0,
349 'added': 0,
350 'deleted': 0,
350 'deleted': 0,
351 'binary': False,
351 'binary': False,
352 'ops': {},
352 'ops': {},
353 }
353 }
354
354
355 if head['deleted_file_mode']:
355 if head['deleted_file_mode']:
356 op = OPS.DEL
356 op = OPS.DEL
357 stats['binary'] = True
357 stats['binary'] = True
358 stats['ops'][DEL_FILENODE] = 'deleted file'
358 stats['ops'][DEL_FILENODE] = 'deleted file'
359
359
360 elif head['new_file_mode']:
360 elif head['new_file_mode']:
361 op = OPS.ADD
361 op = OPS.ADD
362 stats['binary'] = True
362 stats['binary'] = True
363 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
363 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
364 else: # modify operation, can be copy, rename or chmod
364 else: # modify operation, can be copy, rename or chmod
365
365
366 # CHMOD
366 # CHMOD
367 if head['new_mode'] and head['old_mode']:
367 if head['new_mode'] and head['old_mode']:
368 op = OPS.MOD
368 op = OPS.MOD
369 stats['binary'] = True
369 stats['binary'] = True
370 stats['ops'][CHMOD_FILENODE] = (
370 stats['ops'][CHMOD_FILENODE] = (
371 'modified file chmod %s => %s' % (
371 'modified file chmod %s => %s' % (
372 head['old_mode'], head['new_mode']))
372 head['old_mode'], head['new_mode']))
373 # RENAME
373 # RENAME
374 if head['rename_from'] != head['rename_to']:
374 if head['rename_from'] != head['rename_to']:
375 op = OPS.MOD
375 op = OPS.MOD
376 stats['binary'] = True
376 stats['binary'] = True
377 stats['ops'][RENAMED_FILENODE] = (
377 stats['ops'][RENAMED_FILENODE] = (
378 'file renamed from %s to %s' % (
378 'file renamed from %s to %s' % (
379 head['rename_from'], head['rename_to']))
379 head['rename_from'], head['rename_to']))
380 # COPY
380 # COPY
381 if head.get('copy_from') and head.get('copy_to'):
381 if head.get('copy_from') and head.get('copy_to'):
382 op = OPS.MOD
382 op = OPS.MOD
383 stats['binary'] = True
383 stats['binary'] = True
384 stats['ops'][COPIED_FILENODE] = (
384 stats['ops'][COPIED_FILENODE] = (
385 'file copied from %s to %s' % (
385 'file copied from %s to %s' % (
386 head['copy_from'], head['copy_to']))
386 head['copy_from'], head['copy_to']))
387
387
388 # If our new parsed headers didn't match anything fallback to
388 # If our new parsed headers didn't match anything fallback to
389 # old style detection
389 # old style detection
390 if op is None:
390 if op is None:
391 if not head['a_file'] and head['b_file']:
391 if not head['a_file'] and head['b_file']:
392 op = OPS.ADD
392 op = OPS.ADD
393 stats['binary'] = True
393 stats['binary'] = True
394 stats['ops'][NEW_FILENODE] = 'new file'
394 stats['ops'][NEW_FILENODE] = 'new file'
395
395
396 elif head['a_file'] and not head['b_file']:
396 elif head['a_file'] and not head['b_file']:
397 op = OPS.DEL
397 op = OPS.DEL
398 stats['binary'] = True
398 stats['binary'] = True
399 stats['ops'][DEL_FILENODE] = 'deleted file'
399 stats['ops'][DEL_FILENODE] = 'deleted file'
400
400
401 # it's not ADD not DELETE
401 # it's not ADD not DELETE
402 if op is None:
402 if op is None:
403 op = OPS.MOD
403 op = OPS.MOD
404 stats['binary'] = True
404 stats['binary'] = True
405 stats['ops'][MOD_FILENODE] = 'modified file'
405 stats['ops'][MOD_FILENODE] = 'modified file'
406
406
407 # a real non-binary diff
407 # a real non-binary diff
408 if head['a_file'] or head['b_file']:
408 if head['a_file'] or head['b_file']:
409 try:
409 try:
410 raw_diff, chunks, _stats = self._parse_lines(diff)
410 raw_diff, chunks, _stats = self._parse_lines(diff)
411 stats['binary'] = False
411 stats['binary'] = False
412 stats['added'] = _stats[0]
412 stats['added'] = _stats[0]
413 stats['deleted'] = _stats[1]
413 stats['deleted'] = _stats[1]
414 # explicit mark that it's a modified file
414 # explicit mark that it's a modified file
415 if op == OPS.MOD:
415 if op == OPS.MOD:
416 stats['ops'][MOD_FILENODE] = 'modified file'
416 stats['ops'][MOD_FILENODE] = 'modified file'
417 exceeds_limit = len(raw_diff) > self.file_limit
417 exceeds_limit = len(raw_diff) > self.file_limit
418
418
419 # changed from _escaper function so we validate size of
419 # changed from _escaper function so we validate size of
420 # each file instead of the whole diff
420 # each file instead of the whole diff
421 # diff will hide big files but still show small ones
421 # diff will hide big files but still show small ones
422 # from my tests, big files are fairly safe to be parsed
422 # from my tests, big files are fairly safe to be parsed
423 # but the browser is the bottleneck
423 # but the browser is the bottleneck
424 if not self.show_full_diff and exceeds_limit:
424 if not self.show_full_diff and exceeds_limit:
425 raise DiffLimitExceeded('File Limit Exceeded')
425 raise DiffLimitExceeded('File Limit Exceeded')
426
426
427 except DiffLimitExceeded:
427 except DiffLimitExceeded:
428 diff_container = lambda _diff: \
428 diff_container = lambda _diff: \
429 LimitedDiffContainer(
429 LimitedDiffContainer(
430 self.diff_limit, self.cur_diff_size, _diff)
430 self.diff_limit, self.cur_diff_size, _diff)
431
431
432 exceeds_limit = len(raw_diff) > self.file_limit
432 exceeds_limit = len(raw_diff) > self.file_limit
433 limited_diff = True
433 limited_diff = True
434 chunks = []
434 chunks = []
435
435
436 else: # GIT format binary patch, or possibly empty diff
436 else: # GIT format binary patch, or possibly empty diff
437 if head['bin_patch']:
437 if head['bin_patch']:
438 # we have operation already extracted, but we mark simply
438 # we have operation already extracted, but we mark simply
439 # it's a diff we wont show for binary files
439 # it's a diff we wont show for binary files
440 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
440 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
441 chunks = []
441 chunks = []
442
442
443 if chunks and not self.show_full_diff and op == OPS.DEL:
443 if chunks and not self.show_full_diff and op == OPS.DEL:
444 # if not full diff mode show deleted file contents
444 # if not full diff mode show deleted file contents
445 # TODO: anderson: if the view is not too big, there is no way
445 # TODO: anderson: if the view is not too big, there is no way
446 # to see the content of the file
446 # to see the content of the file
447 chunks = []
447 chunks = []
448
448
449 chunks.insert(0, [{
449 chunks.insert(0, [{
450 'old_lineno': '',
450 'old_lineno': '',
451 'new_lineno': '',
451 'new_lineno': '',
452 'action': Action.CONTEXT,
452 'action': Action.CONTEXT,
453 'line': msg,
453 'line': msg,
454 } for _op, msg in stats['ops'].iteritems()
454 } for _op, msg in stats['ops'].iteritems()
455 if _op not in [MOD_FILENODE]])
455 if _op not in [MOD_FILENODE]])
456
456
457 _files.append({
457 _files.append({
458 'filename': safe_unicode(head['b_path']),
458 'filename': safe_unicode(head['b_path']),
459 'old_revision': head['a_blob_id'],
459 'old_revision': head['a_blob_id'],
460 'new_revision': head['b_blob_id'],
460 'new_revision': head['b_blob_id'],
461 'chunks': chunks,
461 'chunks': chunks,
462 'raw_diff': safe_unicode(raw_diff),
462 'raw_diff': safe_unicode(raw_diff),
463 'operation': op,
463 'operation': op,
464 'stats': stats,
464 'stats': stats,
465 'exceeds_limit': exceeds_limit,
465 'exceeds_limit': exceeds_limit,
466 'is_limited_diff': limited_diff,
466 'is_limited_diff': limited_diff,
467 })
467 })
468
468
469 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
469 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
470 OPS.DEL: 2}.get(info['operation'])
470 OPS.DEL: 2}.get(info['operation'])
471
471
472 if not inline_diff:
472 if not inline_diff:
473 return diff_container(sorted(_files, key=sorter))
473 return diff_container(sorted(_files, key=sorter))
474
474
475 # highlight inline changes
475 # highlight inline changes
476 for diff_data in _files:
476 for diff_data in _files:
477 for chunk in diff_data['chunks']:
477 for chunk in diff_data['chunks']:
478 lineiter = iter(chunk)
478 lineiter = iter(chunk)
479 try:
479 try:
480 while 1:
480 while 1:
481 line = lineiter.next()
481 line = lineiter.next()
482 if line['action'] not in (
482 if line['action'] not in (
483 Action.UNMODIFIED, Action.CONTEXT):
483 Action.UNMODIFIED, Action.CONTEXT):
484 nextline = lineiter.next()
484 nextline = lineiter.next()
485 if nextline['action'] in ['unmod', 'context'] or \
485 if nextline['action'] in ['unmod', 'context'] or \
486 nextline['action'] == line['action']:
486 nextline['action'] == line['action']:
487 continue
487 continue
488 self.differ(line, nextline)
488 self.differ(line, nextline)
489 except StopIteration:
489 except StopIteration:
490 pass
490 pass
491
491
492 return diff_container(sorted(_files, key=sorter))
492 return diff_container(sorted(_files, key=sorter))
493
493
494 def _parse_udiff(self, inline_diff=True):
494 def _parse_udiff(self, inline_diff=True):
495 raise NotImplementedError()
495 raise NotImplementedError()
496
496
497 def _parse_lines(self, diff):
497 def _parse_lines(self, diff):
498 """
498 """
499 Parse the diff an return data for the template.
499 Parse the diff an return data for the template.
500 """
500 """
501
501
502 lineiter = iter(diff)
502 lineiter = iter(diff)
503 stats = [0, 0]
503 stats = [0, 0]
504 chunks = []
504 chunks = []
505 raw_diff = []
505 raw_diff = []
506
506
507 try:
507 try:
508 line = lineiter.next()
508 line = lineiter.next()
509
509
510 while line:
510 while line:
511 raw_diff.append(line)
511 raw_diff.append(line)
512 lines = []
512 lines = []
513 chunks.append(lines)
513 chunks.append(lines)
514
514
515 match = self._chunk_re.match(line)
515 match = self._chunk_re.match(line)
516
516
517 if not match:
517 if not match:
518 break
518 break
519
519
520 gr = match.groups()
520 gr = match.groups()
521 (old_line, old_end,
521 (old_line, old_end,
522 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
522 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
523 old_line -= 1
523 old_line -= 1
524 new_line -= 1
524 new_line -= 1
525
525
526 context = len(gr) == 5
526 context = len(gr) == 5
527 old_end += old_line
527 old_end += old_line
528 new_end += new_line
528 new_end += new_line
529
529
530 if context:
530 if context:
531 # skip context only if it's first line
531 # skip context only if it's first line
532 if int(gr[0]) > 1:
532 if int(gr[0]) > 1:
533 lines.append({
533 lines.append({
534 'old_lineno': '...',
534 'old_lineno': '...',
535 'new_lineno': '...',
535 'new_lineno': '...',
536 'action': Action.CONTEXT,
536 'action': Action.CONTEXT,
537 'line': line,
537 'line': line,
538 })
538 })
539
539
540 line = lineiter.next()
540 line = lineiter.next()
541
541
542 while old_line < old_end or new_line < new_end:
542 while old_line < old_end or new_line < new_end:
543 command = ' '
543 command = ' '
544 if line:
544 if line:
545 command = line[0]
545 command = line[0]
546
546
547 affects_old = affects_new = False
547 affects_old = affects_new = False
548
548
549 # ignore those if we don't expect them
549 # ignore those if we don't expect them
550 if command in '#@':
550 if command in '#@':
551 continue
551 continue
552 elif command == '+':
552 elif command == '+':
553 affects_new = True
553 affects_new = True
554 action = Action.ADD
554 action = Action.ADD
555 stats[0] += 1
555 stats[0] += 1
556 elif command == '-':
556 elif command == '-':
557 affects_old = True
557 affects_old = True
558 action = Action.DELETE
558 action = Action.DELETE
559 stats[1] += 1
559 stats[1] += 1
560 else:
560 else:
561 affects_old = affects_new = True
561 affects_old = affects_new = True
562 action = Action.UNMODIFIED
562 action = Action.UNMODIFIED
563
563
564 if not self._newline_marker.match(line):
564 if not self._newline_marker.match(line):
565 old_line += affects_old
565 old_line += affects_old
566 new_line += affects_new
566 new_line += affects_new
567 lines.append({
567 lines.append({
568 'old_lineno': affects_old and old_line or '',
568 'old_lineno': affects_old and old_line or '',
569 'new_lineno': affects_new and new_line or '',
569 'new_lineno': affects_new and new_line or '',
570 'action': action,
570 'action': action,
571 'line': self._clean_line(line, command)
571 'line': self._clean_line(line, command)
572 })
572 })
573 raw_diff.append(line)
573 raw_diff.append(line)
574
574
575 line = lineiter.next()
575 line = lineiter.next()
576
576
577 if self._newline_marker.match(line):
577 if self._newline_marker.match(line):
578 # we need to append to lines, since this is not
578 # we need to append to lines, since this is not
579 # counted in the line specs of diff
579 # counted in the line specs of diff
580 lines.append({
580 lines.append({
581 'old_lineno': '...',
581 'old_lineno': '...',
582 'new_lineno': '...',
582 'new_lineno': '...',
583 'action': Action.CONTEXT,
583 'action': Action.CONTEXT,
584 'line': self._clean_line(line, command)
584 'line': self._clean_line(line, command)
585 })
585 })
586
586
587 except StopIteration:
587 except StopIteration:
588 pass
588 pass
589 return ''.join(raw_diff), chunks, stats
589 return ''.join(raw_diff), chunks, stats
590
590
591 def _safe_id(self, idstring):
591 def _safe_id(self, idstring):
592 """Make a string safe for including in an id attribute.
592 """Make a string safe for including in an id attribute.
593
593
594 The HTML spec says that id attributes 'must begin with
594 The HTML spec says that id attributes 'must begin with
595 a letter ([A-Za-z]) and may be followed by any number
595 a letter ([A-Za-z]) and may be followed by any number
596 of letters, digits ([0-9]), hyphens ("-"), underscores
596 of letters, digits ([0-9]), hyphens ("-"), underscores
597 ("_"), colons (":"), and periods (".")'. These regexps
597 ("_"), colons (":"), and periods (".")'. These regexps
598 are slightly over-zealous, in that they remove colons
598 are slightly over-zealous, in that they remove colons
599 and periods unnecessarily.
599 and periods unnecessarily.
600
600
601 Whitespace is transformed into underscores, and then
601 Whitespace is transformed into underscores, and then
602 anything which is not a hyphen or a character that
602 anything which is not a hyphen or a character that
603 matches \w (alphanumerics and underscore) is removed.
603 matches \w (alphanumerics and underscore) is removed.
604
604
605 """
605 """
606 # Transform all whitespace to underscore
606 # Transform all whitespace to underscore
607 idstring = re.sub(r'\s', "_", '%s' % idstring)
607 idstring = re.sub(r'\s', "_", '%s' % idstring)
608 # Remove everything that is not a hyphen or a member of \w
608 # Remove everything that is not a hyphen or a member of \w
609 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
609 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
610 return idstring
610 return idstring
611
611
612 def prepare(self, inline_diff=True):
612 def prepare(self, inline_diff=True):
613 """
613 """
614 Prepare the passed udiff for HTML rendering.
614 Prepare the passed udiff for HTML rendering.
615
615
616 :return: A list of dicts with diff information.
616 :return: A list of dicts with diff information.
617 """
617 """
618 parsed = self._parser(inline_diff=inline_diff)
618 parsed = self._parser(inline_diff=inline_diff)
619 self.parsed = True
619 self.parsed = True
620 self.parsed_diff = parsed
620 self.parsed_diff = parsed
621 return parsed
621 return parsed
622
622
623 def as_raw(self, diff_lines=None):
623 def as_raw(self, diff_lines=None):
624 """
624 """
625 Returns raw diff as a byte string
625 Returns raw diff as a byte string
626 """
626 """
627 return self._diff.raw
627 return self._diff.raw
628
628
629 def as_html(self, table_class='code-difftable', line_class='line',
629 def as_html(self, table_class='code-difftable', line_class='line',
630 old_lineno_class='lineno old', new_lineno_class='lineno new',
630 old_lineno_class='lineno old', new_lineno_class='lineno new',
631 code_class='code', enable_comments=False, parsed_lines=None):
631 code_class='code', enable_comments=False, parsed_lines=None):
632 """
632 """
633 Return given diff as html table with customized css classes
633 Return given diff as html table with customized css classes
634 """
634 """
635 def _link_to_if(condition, label, url):
635 def _link_to_if(condition, label, url):
636 """
636 """
637 Generates a link if condition is meet or just the label if not.
637 Generates a link if condition is meet or just the label if not.
638 """
638 """
639
639
640 if condition:
640 if condition:
641 return '''<a href="%(url)s" class="tooltip"
641 return '''<a href="%(url)s" class="tooltip"
642 title="%(title)s">%(label)s</a>''' % {
642 title="%(title)s">%(label)s</a>''' % {
643 'title': _('Click to select line'),
643 'title': _('Click to select line'),
644 'url': url,
644 'url': url,
645 'label': label
645 'label': label
646 }
646 }
647 else:
647 else:
648 return label
648 return label
649 if not self.parsed:
649 if not self.parsed:
650 self.prepare()
650 self.prepare()
651
651
652 diff_lines = self.parsed_diff
652 diff_lines = self.parsed_diff
653 if parsed_lines:
653 if parsed_lines:
654 diff_lines = parsed_lines
654 diff_lines = parsed_lines
655
655
656 _html_empty = True
656 _html_empty = True
657 _html = []
657 _html = []
658 _html.append('''<table class="%(table_class)s">\n''' % {
658 _html.append('''<table class="%(table_class)s">\n''' % {
659 'table_class': table_class
659 'table_class': table_class
660 })
660 })
661
661
662 for diff in diff_lines:
662 for diff in diff_lines:
663 for line in diff['chunks']:
663 for line in diff['chunks']:
664 _html_empty = False
664 _html_empty = False
665 for change in line:
665 for change in line:
666 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
666 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
667 'lc': line_class,
667 'lc': line_class,
668 'action': change['action']
668 'action': change['action']
669 })
669 })
670 anchor_old_id = ''
670 anchor_old_id = ''
671 anchor_new_id = ''
671 anchor_new_id = ''
672 anchor_old = "%(filename)s_o%(oldline_no)s" % {
672 anchor_old = "%(filename)s_o%(oldline_no)s" % {
673 'filename': self._safe_id(diff['filename']),
673 'filename': self._safe_id(diff['filename']),
674 'oldline_no': change['old_lineno']
674 'oldline_no': change['old_lineno']
675 }
675 }
676 anchor_new = "%(filename)s_n%(oldline_no)s" % {
676 anchor_new = "%(filename)s_n%(oldline_no)s" % {
677 'filename': self._safe_id(diff['filename']),
677 'filename': self._safe_id(diff['filename']),
678 'oldline_no': change['new_lineno']
678 'oldline_no': change['new_lineno']
679 }
679 }
680 cond_old = (change['old_lineno'] != '...' and
680 cond_old = (change['old_lineno'] != '...' and
681 change['old_lineno'])
681 change['old_lineno'])
682 cond_new = (change['new_lineno'] != '...' and
682 cond_new = (change['new_lineno'] != '...' and
683 change['new_lineno'])
683 change['new_lineno'])
684 if cond_old:
684 if cond_old:
685 anchor_old_id = 'id="%s"' % anchor_old
685 anchor_old_id = 'id="%s"' % anchor_old
686 if cond_new:
686 if cond_new:
687 anchor_new_id = 'id="%s"' % anchor_new
687 anchor_new_id = 'id="%s"' % anchor_new
688
688
689 if change['action'] != Action.CONTEXT:
689 if change['action'] != Action.CONTEXT:
690 anchor_link = True
690 anchor_link = True
691 else:
691 else:
692 anchor_link = False
692 anchor_link = False
693
693
694 ###########################################################
694 ###########################################################
695 # COMMENT ICONS
695 # COMMENT ICONS
696 ###########################################################
696 ###########################################################
697 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
697 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
698
698
699 if enable_comments and change['action'] != Action.CONTEXT:
699 if enable_comments and change['action'] != Action.CONTEXT:
700 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
700 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
701
701
702 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>\n''')
702 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
703
703
704 ###########################################################
704 ###########################################################
705 # OLD LINE NUMBER
705 # OLD LINE NUMBER
706 ###########################################################
706 ###########################################################
707 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
707 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
708 'a_id': anchor_old_id,
708 'a_id': anchor_old_id,
709 'olc': old_lineno_class
709 'olc': old_lineno_class
710 })
710 })
711
711
712 _html.append('''%(link)s''' % {
712 _html.append('''%(link)s''' % {
713 'link': _link_to_if(anchor_link, change['old_lineno'],
713 'link': _link_to_if(anchor_link, change['old_lineno'],
714 '#%s' % anchor_old)
714 '#%s' % anchor_old)
715 })
715 })
716 _html.append('''</td>\n''')
716 _html.append('''</td>\n''')
717 ###########################################################
717 ###########################################################
718 # NEW LINE NUMBER
718 # NEW LINE NUMBER
719 ###########################################################
719 ###########################################################
720
720
721 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
721 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
722 'a_id': anchor_new_id,
722 'a_id': anchor_new_id,
723 'nlc': new_lineno_class
723 'nlc': new_lineno_class
724 })
724 })
725
725
726 _html.append('''%(link)s''' % {
726 _html.append('''%(link)s''' % {
727 'link': _link_to_if(anchor_link, change['new_lineno'],
727 'link': _link_to_if(anchor_link, change['new_lineno'],
728 '#%s' % anchor_new)
728 '#%s' % anchor_new)
729 })
729 })
730 _html.append('''</td>\n''')
730 _html.append('''</td>\n''')
731 ###########################################################
731 ###########################################################
732 # CODE
732 # CODE
733 ###########################################################
733 ###########################################################
734 code_classes = [code_class]
734 code_classes = [code_class]
735 if (not enable_comments or
735 if (not enable_comments or
736 change['action'] == Action.CONTEXT):
736 change['action'] == Action.CONTEXT):
737 code_classes.append('no-comment')
737 code_classes.append('no-comment')
738 _html.append('\t<td class="%s">' % ' '.join(code_classes))
738 _html.append('\t<td class="%s">' % ' '.join(code_classes))
739 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
739 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
740 'code': change['line']
740 'code': change['line']
741 })
741 })
742
742
743 _html.append('''\t</td>''')
743 _html.append('''\t</td>''')
744 _html.append('''\n</tr>\n''')
744 _html.append('''\n</tr>\n''')
745 _html.append('''</table>''')
745 _html.append('''</table>''')
746 if _html_empty:
746 if _html_empty:
747 return None
747 return None
748 return ''.join(_html)
748 return ''.join(_html)
749
749
750 def stat(self):
750 def stat(self):
751 """
751 """
752 Returns tuple of added, and removed lines for this instance
752 Returns tuple of added, and removed lines for this instance
753 """
753 """
754 return self.adds, self.removes
754 return self.adds, self.removes
755
755
756 def get_context_of_line(
756 def get_context_of_line(
757 self, path, diff_line=None, context_before=3, context_after=3):
757 self, path, diff_line=None, context_before=3, context_after=3):
758 """
758 """
759 Returns the context lines for the specified diff line.
759 Returns the context lines for the specified diff line.
760
760
761 :type diff_line: :class:`DiffLineNumber`
761 :type diff_line: :class:`DiffLineNumber`
762 """
762 """
763 assert self.parsed, "DiffProcessor is not initialized."
763 assert self.parsed, "DiffProcessor is not initialized."
764
764
765 if None not in diff_line:
765 if None not in diff_line:
766 raise ValueError(
766 raise ValueError(
767 "Cannot specify both line numbers: {}".format(diff_line))
767 "Cannot specify both line numbers: {}".format(diff_line))
768
768
769 file_diff = self._get_file_diff(path)
769 file_diff = self._get_file_diff(path)
770 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
770 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
771
771
772 first_line_to_include = max(idx - context_before, 0)
772 first_line_to_include = max(idx - context_before, 0)
773 first_line_after_context = idx + context_after + 1
773 first_line_after_context = idx + context_after + 1
774 context_lines = chunk[first_line_to_include:first_line_after_context]
774 context_lines = chunk[first_line_to_include:first_line_after_context]
775
775
776 line_contents = [
776 line_contents = [
777 _context_line(line) for line in context_lines
777 _context_line(line) for line in context_lines
778 if _is_diff_content(line)]
778 if _is_diff_content(line)]
779 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
779 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
780 # Once they are fixed, we can drop this line here.
780 # Once they are fixed, we can drop this line here.
781 if line_contents:
781 if line_contents:
782 line_contents[-1] = (
782 line_contents[-1] = (
783 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
783 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
784 return line_contents
784 return line_contents
785
785
786 def find_context(self, path, context, offset=0):
786 def find_context(self, path, context, offset=0):
787 """
787 """
788 Finds the given `context` inside of the diff.
788 Finds the given `context` inside of the diff.
789
789
790 Use the parameter `offset` to specify which offset the target line has
790 Use the parameter `offset` to specify which offset the target line has
791 inside of the given `context`. This way the correct diff line will be
791 inside of the given `context`. This way the correct diff line will be
792 returned.
792 returned.
793
793
794 :param offset: Shall be used to specify the offset of the main line
794 :param offset: Shall be used to specify the offset of the main line
795 within the given `context`.
795 within the given `context`.
796 """
796 """
797 if offset < 0 or offset >= len(context):
797 if offset < 0 or offset >= len(context):
798 raise ValueError(
798 raise ValueError(
799 "Only positive values up to the length of the context "
799 "Only positive values up to the length of the context "
800 "minus one are allowed.")
800 "minus one are allowed.")
801
801
802 matches = []
802 matches = []
803 file_diff = self._get_file_diff(path)
803 file_diff = self._get_file_diff(path)
804
804
805 for chunk in file_diff['chunks']:
805 for chunk in file_diff['chunks']:
806 context_iter = iter(context)
806 context_iter = iter(context)
807 for line_idx, line in enumerate(chunk):
807 for line_idx, line in enumerate(chunk):
808 try:
808 try:
809 if _context_line(line) == context_iter.next():
809 if _context_line(line) == context_iter.next():
810 continue
810 continue
811 except StopIteration:
811 except StopIteration:
812 matches.append((line_idx, chunk))
812 matches.append((line_idx, chunk))
813 context_iter = iter(context)
813 context_iter = iter(context)
814
814
815 # Increment position and triger StopIteration
815 # Increment position and triger StopIteration
816 # if we had a match at the end
816 # if we had a match at the end
817 line_idx += 1
817 line_idx += 1
818 try:
818 try:
819 context_iter.next()
819 context_iter.next()
820 except StopIteration:
820 except StopIteration:
821 matches.append((line_idx, chunk))
821 matches.append((line_idx, chunk))
822
822
823 effective_offset = len(context) - offset
823 effective_offset = len(context) - offset
824 found_at_diff_lines = [
824 found_at_diff_lines = [
825 _line_to_diff_line_number(chunk[idx - effective_offset])
825 _line_to_diff_line_number(chunk[idx - effective_offset])
826 for idx, chunk in matches]
826 for idx, chunk in matches]
827
827
828 return found_at_diff_lines
828 return found_at_diff_lines
829
829
830 def _get_file_diff(self, path):
830 def _get_file_diff(self, path):
831 for file_diff in self.parsed_diff:
831 for file_diff in self.parsed_diff:
832 if file_diff['filename'] == path:
832 if file_diff['filename'] == path:
833 break
833 break
834 else:
834 else:
835 raise FileNotInDiffException("File {} not in diff".format(path))
835 raise FileNotInDiffException("File {} not in diff".format(path))
836 return file_diff
836 return file_diff
837
837
838 def _find_chunk_line_index(self, file_diff, diff_line):
838 def _find_chunk_line_index(self, file_diff, diff_line):
839 for chunk in file_diff['chunks']:
839 for chunk in file_diff['chunks']:
840 for idx, line in enumerate(chunk):
840 for idx, line in enumerate(chunk):
841 if line['old_lineno'] == diff_line.old:
841 if line['old_lineno'] == diff_line.old:
842 return chunk, idx
842 return chunk, idx
843 if line['new_lineno'] == diff_line.new:
843 if line['new_lineno'] == diff_line.new:
844 return chunk, idx
844 return chunk, idx
845 raise LineNotInDiffException(
845 raise LineNotInDiffException(
846 "The line {} is not part of the diff.".format(diff_line))
846 "The line {} is not part of the diff.".format(diff_line))
847
847
848
848
849 def _is_diff_content(line):
849 def _is_diff_content(line):
850 return line['action'] in (
850 return line['action'] in (
851 Action.UNMODIFIED, Action.ADD, Action.DELETE)
851 Action.UNMODIFIED, Action.ADD, Action.DELETE)
852
852
853
853
854 def _context_line(line):
854 def _context_line(line):
855 return (line['action'], line['line'])
855 return (line['action'], line['line'])
856
856
857
857
858 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
858 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
859
859
860
860
861 def _line_to_diff_line_number(line):
861 def _line_to_diff_line_number(line):
862 new_line_no = line['new_lineno'] or None
862 new_line_no = line['new_lineno'] or None
863 old_line_no = line['old_lineno'] or None
863 old_line_no = line['old_lineno'] or None
864 return DiffLineNumber(old=old_line_no, new=new_line_no)
864 return DiffLineNumber(old=old_line_no, new=new_line_no)
865
865
866
866
867 class FileNotInDiffException(Exception):
867 class FileNotInDiffException(Exception):
868 """
868 """
869 Raised when the context for a missing file is requested.
869 Raised when the context for a missing file is requested.
870
870
871 If you request the context for a line in a file which is not part of the
871 If you request the context for a line in a file which is not part of the
872 given diff, then this exception is raised.
872 given diff, then this exception is raised.
873 """
873 """
874
874
875
875
876 class LineNotInDiffException(Exception):
876 class LineNotInDiffException(Exception):
877 """
877 """
878 Raised when the context for a missing line is requested.
878 Raised when the context for a missing line is requested.
879
879
880 If you request the context for a line in a file and this line is not
880 If you request the context for a line in a file and this line is not
881 part of the given diff, then this exception is raised.
881 part of the given diff, then this exception is raised.
882 """
882 """
883
883
884
884
885 class DiffLimitExceeded(Exception):
885 class DiffLimitExceeded(Exception):
886 pass
886 pass
@@ -1,748 +1,748 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import textwrap
21 import textwrap
22
22
23 import pytest
23 import pytest
24
24
25 from rhodecode.lib.diffs import (
25 from rhodecode.lib.diffs import (
26 DiffProcessor, wrapped_diff,
26 DiffProcessor, wrapped_diff,
27 NEW_FILENODE, DEL_FILENODE, MOD_FILENODE, RENAMED_FILENODE,
27 NEW_FILENODE, DEL_FILENODE, MOD_FILENODE, RENAMED_FILENODE,
28 CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE)
28 CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE)
29 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.fixture import Fixture
30 from rhodecode.lib.vcs.backends.git.repository import GitDiff
30 from rhodecode.lib.vcs.backends.git.repository import GitDiff
31 from rhodecode.lib.vcs.backends.hg.repository import MercurialDiff
31 from rhodecode.lib.vcs.backends.hg.repository import MercurialDiff
32 from rhodecode.lib.vcs.backends.svn.repository import SubversionDiff
32 from rhodecode.lib.vcs.backends.svn.repository import SubversionDiff
33
33
34 fixture = Fixture()
34 fixture = Fixture()
35
35
36
36
37 def test_wrapped_diff_limited_file_diff(vcsbackend_random):
37 def test_wrapped_diff_limited_file_diff(vcsbackend_random):
38 vcsbackend = vcsbackend_random
38 vcsbackend = vcsbackend_random
39 repo = vcsbackend.create_repo()
39 repo = vcsbackend.create_repo()
40 vcsbackend.add_file(repo, 'a_file', content="line 1\nline 2\nline3\n")
40 vcsbackend.add_file(repo, 'a_file', content="line 1\nline 2\nline3\n")
41 commit = repo.get_commit()
41 commit = repo.get_commit()
42 file_node = commit.get_node('a_file')
42 file_node = commit.get_node('a_file')
43
43
44 # Only limit the file diff to trigger the code path
44 # Only limit the file diff to trigger the code path
45 result = wrapped_diff(
45 result = wrapped_diff(
46 None, file_node, diff_limit=10000, file_limit=1)
46 None, file_node, diff_limit=10000, file_limit=1)
47 data = result[5]
47 data = result[5]
48
48
49 # Verify that the limits were applied
49 # Verify that the limits were applied
50 assert data['exceeds_limit'] is True
50 assert data['exceeds_limit'] is True
51 assert data['is_limited_diff'] is True
51 assert data['is_limited_diff'] is True
52
52
53
53
54 def test_diffprocessor_as_html_with_comments():
54 def test_diffprocessor_as_html_with_comments():
55 raw_diff = textwrap.dedent('''
55 raw_diff = textwrap.dedent('''
56 diff --git a/setup.py b/setup.py
56 diff --git a/setup.py b/setup.py
57 index 5b36422..cfd698e 100755
57 index 5b36422..cfd698e 100755
58 --- a/setup.py
58 --- a/setup.py
59 +++ b/setup.py
59 +++ b/setup.py
60 @@ -2,7 +2,7 @@
60 @@ -2,7 +2,7 @@
61 #!/usr/bin/python
61 #!/usr/bin/python
62 # Setup file for X
62 # Setup file for X
63 # Copyright (C) No one
63 # Copyright (C) No one
64 -
64 -
65 +x
65 +x
66 try:
66 try:
67 from setuptools import setup, Extension
67 from setuptools import setup, Extension
68 except ImportError:
68 except ImportError:
69 ''')
69 ''')
70 diff = GitDiff(raw_diff)
70 diff = GitDiff(raw_diff)
71 processor = DiffProcessor(diff)
71 processor = DiffProcessor(diff)
72 processor.prepare()
72 processor.prepare()
73
73
74 # Note that the cell with the context in line 5 (in the html) has the
74 # Note that the cell with the context in line 5 (in the html) has the
75 # no-comment class, which will prevent the add comment icon to be displayed.
75 # no-comment class, which will prevent the add comment icon to be displayed.
76 expected_html = textwrap.dedent('''
76 expected_html = textwrap.dedent('''
77 <table class="code-difftable">
77 <table class="code-difftable">
78 <tr class="line context">
78 <tr class="line context">
79 <td class="add-comment-line"><span class="add-comment-content"></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
79 <td class="add-comment-line"><span class="add-comment-content"></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
80 <td class="lineno old">...</td>
80 <td class="lineno old">...</td>
81 <td class="lineno new">...</td>
81 <td class="lineno new">...</td>
82 <td class="code no-comment">
82 <td class="code no-comment">
83 <pre>@@ -2,7 +2,7 @@
83 <pre>@@ -2,7 +2,7 @@
84 </pre>
84 </pre>
85 </td>
85 </td>
86 </tr>
86 </tr>
87 <tr class="line unmod">
87 <tr class="line unmod">
88 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
88 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
89 <td id="setuppy_o2" class="lineno old"><a href="#setuppy_o2" class="tooltip"
89 <td id="setuppy_o2" class="lineno old"><a href="#setuppy_o2" class="tooltip"
90 title="Click to select line">2</a></td>
90 title="Click to select line">2</a></td>
91 <td id="setuppy_n2" class="lineno new"><a href="#setuppy_n2" class="tooltip"
91 <td id="setuppy_n2" class="lineno new"><a href="#setuppy_n2" class="tooltip"
92 title="Click to select line">2</a></td>
92 title="Click to select line">2</a></td>
93 <td class="code">
93 <td class="code">
94 <pre>#!/usr/bin/python
94 <pre>#!/usr/bin/python
95 </pre>
95 </pre>
96 </td>
96 </td>
97 </tr>
97 </tr>
98 <tr class="line unmod">
98 <tr class="line unmod">
99 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
99 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
100 <td id="setuppy_o3" class="lineno old"><a href="#setuppy_o3" class="tooltip"
100 <td id="setuppy_o3" class="lineno old"><a href="#setuppy_o3" class="tooltip"
101 title="Click to select line">3</a></td>
101 title="Click to select line">3</a></td>
102 <td id="setuppy_n3" class="lineno new"><a href="#setuppy_n3" class="tooltip"
102 <td id="setuppy_n3" class="lineno new"><a href="#setuppy_n3" class="tooltip"
103 title="Click to select line">3</a></td>
103 title="Click to select line">3</a></td>
104 <td class="code">
104 <td class="code">
105 <pre># Setup file for X
105 <pre># Setup file for X
106 </pre>
106 </pre>
107 </td>
107 </td>
108 </tr>
108 </tr>
109 <tr class="line unmod">
109 <tr class="line unmod">
110 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
110 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
111 <td id="setuppy_o4" class="lineno old"><a href="#setuppy_o4" class="tooltip"
111 <td id="setuppy_o4" class="lineno old"><a href="#setuppy_o4" class="tooltip"
112 title="Click to select line">4</a></td>
112 title="Click to select line">4</a></td>
113 <td id="setuppy_n4" class="lineno new"><a href="#setuppy_n4" class="tooltip"
113 <td id="setuppy_n4" class="lineno new"><a href="#setuppy_n4" class="tooltip"
114 title="Click to select line">4</a></td>
114 title="Click to select line">4</a></td>
115 <td class="code">
115 <td class="code">
116 <pre># Copyright (C) No one
116 <pre># Copyright (C) No one
117 </pre>
117 </pre>
118 </td>
118 </td>
119 </tr>
119 </tr>
120 <tr class="line del">
120 <tr class="line del">
121 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
121 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
122 <td id="setuppy_o5" class="lineno old"><a href="#setuppy_o5" class="tooltip"
122 <td id="setuppy_o5" class="lineno old"><a href="#setuppy_o5" class="tooltip"
123 title="Click to select line">5</a></td>
123 title="Click to select line">5</a></td>
124 <td class="lineno new"><a href="#setuppy_n" class="tooltip"
124 <td class="lineno new"><a href="#setuppy_n" class="tooltip"
125 title="Click to select line"></a></td>
125 title="Click to select line"></a></td>
126 <td class="code">
126 <td class="code">
127 <pre>
127 <pre>
128 </pre>
128 </pre>
129 </td>
129 </td>
130 </tr>
130 </tr>
131 <tr class="line add">
131 <tr class="line add">
132 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
132 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
133 <td class="lineno old"><a href="#setuppy_o" class="tooltip"
133 <td class="lineno old"><a href="#setuppy_o" class="tooltip"
134 title="Click to select line"></a></td>
134 title="Click to select line"></a></td>
135 <td id="setuppy_n5" class="lineno new"><a href="#setuppy_n5" class="tooltip"
135 <td id="setuppy_n5" class="lineno new"><a href="#setuppy_n5" class="tooltip"
136 title="Click to select line">5</a></td>
136 title="Click to select line">5</a></td>
137 <td class="code">
137 <td class="code">
138 <pre><ins>x</ins>
138 <pre><ins>x</ins>
139 </pre>
139 </pre>
140 </td>
140 </td>
141 </tr>
141 </tr>
142 <tr class="line unmod">
142 <tr class="line unmod">
143 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
143 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
144 <td id="setuppy_o6" class="lineno old"><a href="#setuppy_o6" class="tooltip"
144 <td id="setuppy_o6" class="lineno old"><a href="#setuppy_o6" class="tooltip"
145 title="Click to select line">6</a></td>
145 title="Click to select line">6</a></td>
146 <td id="setuppy_n6" class="lineno new"><a href="#setuppy_n6" class="tooltip"
146 <td id="setuppy_n6" class="lineno new"><a href="#setuppy_n6" class="tooltip"
147 title="Click to select line">6</a></td>
147 title="Click to select line">6</a></td>
148 <td class="code">
148 <td class="code">
149 <pre>try:
149 <pre>try:
150 </pre>
150 </pre>
151 </td>
151 </td>
152 </tr>
152 </tr>
153 <tr class="line unmod">
153 <tr class="line unmod">
154 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
154 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
155 <td id="setuppy_o7" class="lineno old"><a href="#setuppy_o7" class="tooltip"
155 <td id="setuppy_o7" class="lineno old"><a href="#setuppy_o7" class="tooltip"
156 title="Click to select line">7</a></td>
156 title="Click to select line">7</a></td>
157 <td id="setuppy_n7" class="lineno new"><a href="#setuppy_n7" class="tooltip"
157 <td id="setuppy_n7" class="lineno new"><a href="#setuppy_n7" class="tooltip"
158 title="Click to select line">7</a></td>
158 title="Click to select line">7</a></td>
159 <td class="code">
159 <td class="code">
160 <pre> from setuptools import setup, Extension
160 <pre> from setuptools import setup, Extension
161 </pre>
161 </pre>
162 </td>
162 </td>
163 </tr>
163 </tr>
164 <tr class="line unmod">
164 <tr class="line unmod">
165 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>
165 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>
166 <td id="setuppy_o8" class="lineno old"><a href="#setuppy_o8" class="tooltip"
166 <td id="setuppy_o8" class="lineno old"><a href="#setuppy_o8" class="tooltip"
167 title="Click to select line">8</a></td>
167 title="Click to select line">8</a></td>
168 <td id="setuppy_n8" class="lineno new"><a href="#setuppy_n8" class="tooltip"
168 <td id="setuppy_n8" class="lineno new"><a href="#setuppy_n8" class="tooltip"
169 title="Click to select line">8</a></td>
169 title="Click to select line">8</a></td>
170 <td class="code">
170 <td class="code">
171 <pre>except ImportError:
171 <pre>except ImportError:
172 </pre>
172 </pre>
173 </td>
173 </td>
174 </tr>
174 </tr>
175 </table>
175 </table>
176 ''').strip()
176 ''').strip()
177 html = processor.as_html(enable_comments=True).replace('\t', ' ')
177 html = processor.as_html(enable_comments=True).replace('\t', ' ')
178
178
179 assert html == expected_html
179 assert html == expected_html
180
180
181
181
182 class TestMixedFilenameEncodings:
182 class TestMixedFilenameEncodings:
183
183
184 @pytest.fixture(scope="class")
184 @pytest.fixture(scope="class")
185 def raw_diff(self):
185 def raw_diff(self):
186 return fixture.load_resource(
186 return fixture.load_resource(
187 'hg_diff_mixed_filename_encodings.diff')
187 'hg_diff_mixed_filename_encodings.diff')
188
188
189 @pytest.fixture
189 @pytest.fixture
190 def processor(self, raw_diff):
190 def processor(self, raw_diff):
191 diff = MercurialDiff(raw_diff)
191 diff = MercurialDiff(raw_diff)
192 processor = DiffProcessor(diff)
192 processor = DiffProcessor(diff)
193 return processor
193 return processor
194
194
195 def test_filenames_are_decoded_to_unicode(self, processor):
195 def test_filenames_are_decoded_to_unicode(self, processor):
196 diff_data = processor.prepare()
196 diff_data = processor.prepare()
197 filenames = [item['filename'] for item in diff_data]
197 filenames = [item['filename'] for item in diff_data]
198 assert filenames == [
198 assert filenames == [
199 u'spΓ€cial-utf8.txt', u'spοΏ½cial-cp1252.txt', u'spοΏ½cial-latin1.txt']
199 u'spΓ€cial-utf8.txt', u'spοΏ½cial-cp1252.txt', u'spοΏ½cial-latin1.txt']
200
200
201 def test_raw_diff_is_decoded_to_unicode(self, processor):
201 def test_raw_diff_is_decoded_to_unicode(self, processor):
202 diff_data = processor.prepare()
202 diff_data = processor.prepare()
203 raw_diffs = [item['raw_diff'] for item in diff_data]
203 raw_diffs = [item['raw_diff'] for item in diff_data]
204 new_file_message = u'\nnew file mode 100644\n'
204 new_file_message = u'\nnew file mode 100644\n'
205 expected_raw_diffs = [
205 expected_raw_diffs = [
206 u' a/spΓ€cial-utf8.txt b/spΓ€cial-utf8.txt' + new_file_message,
206 u' a/spΓ€cial-utf8.txt b/spΓ€cial-utf8.txt' + new_file_message,
207 u' a/spοΏ½cial-cp1252.txt b/spοΏ½cial-cp1252.txt' + new_file_message,
207 u' a/spοΏ½cial-cp1252.txt b/spοΏ½cial-cp1252.txt' + new_file_message,
208 u' a/spοΏ½cial-latin1.txt b/spοΏ½cial-latin1.txt' + new_file_message]
208 u' a/spοΏ½cial-latin1.txt b/spοΏ½cial-latin1.txt' + new_file_message]
209 assert raw_diffs == expected_raw_diffs
209 assert raw_diffs == expected_raw_diffs
210
210
211 def test_as_raw_preserves_the_encoding(self, processor, raw_diff):
211 def test_as_raw_preserves_the_encoding(self, processor, raw_diff):
212 assert processor.as_raw() == raw_diff
212 assert processor.as_raw() == raw_diff
213
213
214
214
215 # TODO: mikhail: format the following data structure properly
215 # TODO: mikhail: format the following data structure properly
216 DIFF_FIXTURES = [
216 DIFF_FIXTURES = [
217 ('hg',
217 ('hg',
218 'hg_diff_add_single_binary_file.diff',
218 'hg_diff_add_single_binary_file.diff',
219 [('US Warszawa.jpg', 'A',
219 [('US Warszawa.jpg', 'A',
220 {'added': 0,
220 {'added': 0,
221 'deleted': 0,
221 'deleted': 0,
222 'binary': True,
222 'binary': True,
223 'ops': {NEW_FILENODE: 'new file 100755',
223 'ops': {NEW_FILENODE: 'new file 100755',
224 BIN_FILENODE: 'binary diff hidden'}}),
224 BIN_FILENODE: 'binary diff hidden'}}),
225 ]),
225 ]),
226 ('hg',
226 ('hg',
227 'hg_diff_mod_single_binary_file.diff',
227 'hg_diff_mod_single_binary_file.diff',
228 [('US Warszawa.jpg', 'M',
228 [('US Warszawa.jpg', 'M',
229 {'added': 0,
229 {'added': 0,
230 'deleted': 0,
230 'deleted': 0,
231 'binary': True,
231 'binary': True,
232 'ops': {MOD_FILENODE: 'modified file',
232 'ops': {MOD_FILENODE: 'modified file',
233 BIN_FILENODE: 'binary diff hidden'}}),
233 BIN_FILENODE: 'binary diff hidden'}}),
234 ]),
234 ]),
235 ('hg',
235 ('hg',
236 'hg_diff_mod_single_file_and_rename_and_chmod.diff',
236 'hg_diff_mod_single_file_and_rename_and_chmod.diff',
237 [('README', 'M',
237 [('README', 'M',
238 {'added': 3,
238 {'added': 3,
239 'deleted': 0,
239 'deleted': 0,
240 'binary': False,
240 'binary': False,
241 'ops': {MOD_FILENODE: 'modified file',
241 'ops': {MOD_FILENODE: 'modified file',
242 RENAMED_FILENODE: 'file renamed from README.rst to README',
242 RENAMED_FILENODE: 'file renamed from README.rst to README',
243 CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
243 CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
244 ]),
244 ]),
245 ('hg',
245 ('hg',
246 'hg_diff_mod_file_and_rename.diff',
246 'hg_diff_mod_file_and_rename.diff',
247 [('README.rst', 'M',
247 [('README.rst', 'M',
248 {'added': 3,
248 {'added': 3,
249 'deleted': 0,
249 'deleted': 0,
250 'binary': False,
250 'binary': False,
251 'ops': {MOD_FILENODE: 'modified file',
251 'ops': {MOD_FILENODE: 'modified file',
252 RENAMED_FILENODE: 'file renamed from README to README.rst'}}),
252 RENAMED_FILENODE: 'file renamed from README to README.rst'}}),
253 ]),
253 ]),
254 ('hg',
254 ('hg',
255 'hg_diff_del_single_binary_file.diff',
255 'hg_diff_del_single_binary_file.diff',
256 [('US Warszawa.jpg', 'D',
256 [('US Warszawa.jpg', 'D',
257 {'added': 0,
257 {'added': 0,
258 'deleted': 0,
258 'deleted': 0,
259 'binary': True,
259 'binary': True,
260 'ops': {DEL_FILENODE: 'deleted file',
260 'ops': {DEL_FILENODE: 'deleted file',
261 BIN_FILENODE: 'binary diff hidden'}}),
261 BIN_FILENODE: 'binary diff hidden'}}),
262 ]),
262 ]),
263 ('hg',
263 ('hg',
264 'hg_diff_chmod_and_mod_single_binary_file.diff',
264 'hg_diff_chmod_and_mod_single_binary_file.diff',
265 [('gravatar.png', 'M',
265 [('gravatar.png', 'M',
266 {'added': 0,
266 {'added': 0,
267 'deleted': 0,
267 'deleted': 0,
268 'binary': True,
268 'binary': True,
269 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
269 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
270 BIN_FILENODE: 'binary diff hidden'}}),
270 BIN_FILENODE: 'binary diff hidden'}}),
271 ]),
271 ]),
272 ('hg',
272 ('hg',
273 'hg_diff_chmod.diff',
273 'hg_diff_chmod.diff',
274 [('file', 'M',
274 [('file', 'M',
275 {'added': 0,
275 {'added': 0,
276 'deleted': 0,
276 'deleted': 0,
277 'binary': True,
277 'binary': True,
278 'ops': {CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
278 'ops': {CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
279 ]),
279 ]),
280 ('hg',
280 ('hg',
281 'hg_diff_rename_file.diff',
281 'hg_diff_rename_file.diff',
282 [('file_renamed', 'M',
282 [('file_renamed', 'M',
283 {'added': 0,
283 {'added': 0,
284 'deleted': 0,
284 'deleted': 0,
285 'binary': True,
285 'binary': True,
286 'ops': {RENAMED_FILENODE: 'file renamed from file to file_renamed'}}),
286 'ops': {RENAMED_FILENODE: 'file renamed from file to file_renamed'}}),
287 ]),
287 ]),
288 ('hg',
288 ('hg',
289 'hg_diff_rename_and_chmod_file.diff',
289 'hg_diff_rename_and_chmod_file.diff',
290 [('README', 'M',
290 [('README', 'M',
291 {'added': 0,
291 {'added': 0,
292 'deleted': 0,
292 'deleted': 0,
293 'binary': True,
293 'binary': True,
294 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
294 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
295 RENAMED_FILENODE: 'file renamed from README.rst to README'}}),
295 RENAMED_FILENODE: 'file renamed from README.rst to README'}}),
296 ]),
296 ]),
297 ('hg',
297 ('hg',
298 'hg_diff_binary_and_normal.diff',
298 'hg_diff_binary_and_normal.diff',
299 [('img/baseline-10px.png', 'A',
299 [('img/baseline-10px.png', 'A',
300 {'added': 0,
300 {'added': 0,
301 'deleted': 0,
301 'deleted': 0,
302 'binary': True,
302 'binary': True,
303 'ops': {NEW_FILENODE: 'new file 100644',
303 'ops': {NEW_FILENODE: 'new file 100644',
304 BIN_FILENODE: 'binary diff hidden'}}),
304 BIN_FILENODE: 'binary diff hidden'}}),
305 ('js/jquery/hashgrid.js', 'A',
305 ('js/jquery/hashgrid.js', 'A',
306 {'added': 340,
306 {'added': 340,
307 'deleted': 0,
307 'deleted': 0,
308 'binary': False,
308 'binary': False,
309 'ops': {NEW_FILENODE: 'new file 100755'}}),
309 'ops': {NEW_FILENODE: 'new file 100755'}}),
310 ('index.html', 'M',
310 ('index.html', 'M',
311 {'added': 3,
311 {'added': 3,
312 'deleted': 2,
312 'deleted': 2,
313 'binary': False,
313 'binary': False,
314 'ops': {MOD_FILENODE: 'modified file'}}),
314 'ops': {MOD_FILENODE: 'modified file'}}),
315 ('less/docs.less', 'M',
315 ('less/docs.less', 'M',
316 {'added': 34,
316 {'added': 34,
317 'deleted': 0,
317 'deleted': 0,
318 'binary': False,
318 'binary': False,
319 'ops': {MOD_FILENODE: 'modified file'}}),
319 'ops': {MOD_FILENODE: 'modified file'}}),
320 ('less/scaffolding.less', 'M',
320 ('less/scaffolding.less', 'M',
321 {'added': 1,
321 {'added': 1,
322 'deleted': 3,
322 'deleted': 3,
323 'binary': False,
323 'binary': False,
324 'ops': {MOD_FILENODE: 'modified file'}}),
324 'ops': {MOD_FILENODE: 'modified file'}}),
325 ('readme.markdown', 'M',
325 ('readme.markdown', 'M',
326 {'added': 1,
326 {'added': 1,
327 'deleted': 10,
327 'deleted': 10,
328 'binary': False,
328 'binary': False,
329 'ops': {MOD_FILENODE: 'modified file'}}),
329 'ops': {MOD_FILENODE: 'modified file'}}),
330 ('img/baseline-20px.png', 'D',
330 ('img/baseline-20px.png', 'D',
331 {'added': 0,
331 {'added': 0,
332 'deleted': 0,
332 'deleted': 0,
333 'binary': True,
333 'binary': True,
334 'ops': {DEL_FILENODE: 'deleted file',
334 'ops': {DEL_FILENODE: 'deleted file',
335 BIN_FILENODE: 'binary diff hidden'}}),
335 BIN_FILENODE: 'binary diff hidden'}}),
336 ('js/global.js', 'D',
336 ('js/global.js', 'D',
337 {'added': 0,
337 {'added': 0,
338 'deleted': 75,
338 'deleted': 75,
339 'binary': False,
339 'binary': False,
340 'ops': {DEL_FILENODE: 'deleted file'}})
340 'ops': {DEL_FILENODE: 'deleted file'}})
341 ]),
341 ]),
342 ('git',
342 ('git',
343 'git_diff_chmod.diff',
343 'git_diff_chmod.diff',
344 [('work-horus.xls', 'M',
344 [('work-horus.xls', 'M',
345 {'added': 0,
345 {'added': 0,
346 'deleted': 0,
346 'deleted': 0,
347 'binary': True,
347 'binary': True,
348 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}})
348 'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}})
349 ]),
349 ]),
350 ('git',
350 ('git',
351 'git_diff_rename_file.diff',
351 'git_diff_rename_file.diff',
352 [('file.xls', 'M',
352 [('file.xls', 'M',
353 {'added': 0,
353 {'added': 0,
354 'deleted': 0,
354 'deleted': 0,
355 'binary': True,
355 'binary': True,
356 'ops': {
356 'ops': {
357 RENAMED_FILENODE: 'file renamed from work-horus.xls to file.xls'}})
357 RENAMED_FILENODE: 'file renamed from work-horus.xls to file.xls'}})
358 ]),
358 ]),
359 ('git',
359 ('git',
360 'git_diff_mod_single_binary_file.diff',
360 'git_diff_mod_single_binary_file.diff',
361 [('US Warszawa.jpg', 'M',
361 [('US Warszawa.jpg', 'M',
362 {'added': 0,
362 {'added': 0,
363 'deleted': 0,
363 'deleted': 0,
364 'binary': True,
364 'binary': True,
365 'ops': {MOD_FILENODE: 'modified file',
365 'ops': {MOD_FILENODE: 'modified file',
366 BIN_FILENODE: 'binary diff hidden'}})
366 BIN_FILENODE: 'binary diff hidden'}})
367 ]),
367 ]),
368 ('git',
368 ('git',
369 'git_diff_binary_and_normal.diff',
369 'git_diff_binary_and_normal.diff',
370 [('img/baseline-10px.png', 'A',
370 [('img/baseline-10px.png', 'A',
371 {'added': 0,
371 {'added': 0,
372 'deleted': 0,
372 'deleted': 0,
373 'binary': True,
373 'binary': True,
374 'ops': {NEW_FILENODE: 'new file 100644',
374 'ops': {NEW_FILENODE: 'new file 100644',
375 BIN_FILENODE: 'binary diff hidden'}}),
375 BIN_FILENODE: 'binary diff hidden'}}),
376 ('js/jquery/hashgrid.js', 'A',
376 ('js/jquery/hashgrid.js', 'A',
377 {'added': 340,
377 {'added': 340,
378 'deleted': 0,
378 'deleted': 0,
379 'binary': False,
379 'binary': False,
380 'ops': {NEW_FILENODE: 'new file 100755'}}),
380 'ops': {NEW_FILENODE: 'new file 100755'}}),
381 ('index.html', 'M',
381 ('index.html', 'M',
382 {'added': 3,
382 {'added': 3,
383 'deleted': 2,
383 'deleted': 2,
384 'binary': False,
384 'binary': False,
385 'ops': {MOD_FILENODE: 'modified file'}}),
385 'ops': {MOD_FILENODE: 'modified file'}}),
386 ('less/docs.less', 'M',
386 ('less/docs.less', 'M',
387 {'added': 34,
387 {'added': 34,
388 'deleted': 0,
388 'deleted': 0,
389 'binary': False,
389 'binary': False,
390 'ops': {MOD_FILENODE: 'modified file'}}),
390 'ops': {MOD_FILENODE: 'modified file'}}),
391 ('less/scaffolding.less', 'M',
391 ('less/scaffolding.less', 'M',
392 {'added': 1,
392 {'added': 1,
393 'deleted': 3,
393 'deleted': 3,
394 'binary': False,
394 'binary': False,
395 'ops': {MOD_FILENODE: 'modified file'}}),
395 'ops': {MOD_FILENODE: 'modified file'}}),
396 ('readme.markdown', 'M',
396 ('readme.markdown', 'M',
397 {'added': 1,
397 {'added': 1,
398 'deleted': 10,
398 'deleted': 10,
399 'binary': False,
399 'binary': False,
400 'ops': {MOD_FILENODE: 'modified file'}}),
400 'ops': {MOD_FILENODE: 'modified file'}}),
401 ('img/baseline-20px.png', 'D',
401 ('img/baseline-20px.png', 'D',
402 {'added': 0,
402 {'added': 0,
403 'deleted': 0,
403 'deleted': 0,
404 'binary': True,
404 'binary': True,
405 'ops': {DEL_FILENODE: 'deleted file',
405 'ops': {DEL_FILENODE: 'deleted file',
406 BIN_FILENODE: 'binary diff hidden'}}),
406 BIN_FILENODE: 'binary diff hidden'}}),
407 ('js/global.js', 'D',
407 ('js/global.js', 'D',
408 {'added': 0,
408 {'added': 0,
409 'deleted': 75,
409 'deleted': 75,
410 'binary': False,
410 'binary': False,
411 'ops': {DEL_FILENODE: 'deleted file'}}),
411 'ops': {DEL_FILENODE: 'deleted file'}}),
412 ]),
412 ]),
413 ('hg',
413 ('hg',
414 'diff_with_diff_data.diff',
414 'diff_with_diff_data.diff',
415 [('vcs/backends/base.py', 'M',
415 [('vcs/backends/base.py', 'M',
416 {'added': 18,
416 {'added': 18,
417 'deleted': 2,
417 'deleted': 2,
418 'binary': False,
418 'binary': False,
419 'ops': {MOD_FILENODE: 'modified file'}}),
419 'ops': {MOD_FILENODE: 'modified file'}}),
420 ('vcs/backends/git/repository.py', 'M',
420 ('vcs/backends/git/repository.py', 'M',
421 {'added': 46,
421 {'added': 46,
422 'deleted': 15,
422 'deleted': 15,
423 'binary': False,
423 'binary': False,
424 'ops': {MOD_FILENODE: 'modified file'}}),
424 'ops': {MOD_FILENODE: 'modified file'}}),
425 ('vcs/backends/hg.py', 'M',
425 ('vcs/backends/hg.py', 'M',
426 {'added': 22,
426 {'added': 22,
427 'deleted': 3,
427 'deleted': 3,
428 'binary': False,
428 'binary': False,
429 'ops': {MOD_FILENODE: 'modified file'}}),
429 'ops': {MOD_FILENODE: 'modified file'}}),
430 ('vcs/tests/test_git.py', 'M',
430 ('vcs/tests/test_git.py', 'M',
431 {'added': 5,
431 {'added': 5,
432 'deleted': 5,
432 'deleted': 5,
433 'binary': False,
433 'binary': False,
434 'ops': {MOD_FILENODE: 'modified file'}}),
434 'ops': {MOD_FILENODE: 'modified file'}}),
435 ('vcs/tests/test_repository.py', 'M',
435 ('vcs/tests/test_repository.py', 'M',
436 {'added': 174,
436 {'added': 174,
437 'deleted': 2,
437 'deleted': 2,
438 'binary': False,
438 'binary': False,
439 'ops': {MOD_FILENODE: 'modified file'}}),
439 'ops': {MOD_FILENODE: 'modified file'}}),
440 ]),
440 ]),
441 ('hg',
441 ('hg',
442 'hg_diff_copy_file.diff',
442 'hg_diff_copy_file.diff',
443 [('file2', 'M',
443 [('file2', 'M',
444 {'added': 0,
444 {'added': 0,
445 'deleted': 0,
445 'deleted': 0,
446 'binary': True,
446 'binary': True,
447 'ops': {COPIED_FILENODE: 'file copied from file1 to file2'}}),
447 'ops': {COPIED_FILENODE: 'file copied from file1 to file2'}}),
448 ]),
448 ]),
449 ('hg',
449 ('hg',
450 'hg_diff_copy_and_modify_file.diff',
450 'hg_diff_copy_and_modify_file.diff',
451 [('file3', 'M',
451 [('file3', 'M',
452 {'added': 1,
452 {'added': 1,
453 'deleted': 0,
453 'deleted': 0,
454 'binary': False,
454 'binary': False,
455 'ops': {COPIED_FILENODE: 'file copied from file2 to file3',
455 'ops': {COPIED_FILENODE: 'file copied from file2 to file3',
456 MOD_FILENODE: 'modified file'}}),
456 MOD_FILENODE: 'modified file'}}),
457 ]),
457 ]),
458 ('hg',
458 ('hg',
459 'hg_diff_copy_and_chmod_file.diff',
459 'hg_diff_copy_and_chmod_file.diff',
460 [('file4', 'M',
460 [('file4', 'M',
461 {'added': 0,
461 {'added': 0,
462 'deleted': 0,
462 'deleted': 0,
463 'binary': True,
463 'binary': True,
464 'ops': {COPIED_FILENODE: 'file copied from file3 to file4',
464 'ops': {COPIED_FILENODE: 'file copied from file3 to file4',
465 CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}}),
465 CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}}),
466 ]),
466 ]),
467 ('hg',
467 ('hg',
468 'hg_diff_copy_chmod_and_edit_file.diff',
468 'hg_diff_copy_chmod_and_edit_file.diff',
469 [('file5', 'M',
469 [('file5', 'M',
470 {'added': 2,
470 {'added': 2,
471 'deleted': 1,
471 'deleted': 1,
472 'binary': False,
472 'binary': False,
473 'ops': {COPIED_FILENODE: 'file copied from file4 to file5',
473 'ops': {COPIED_FILENODE: 'file copied from file4 to file5',
474 CHMOD_FILENODE: 'modified file chmod 100755 => 100644',
474 CHMOD_FILENODE: 'modified file chmod 100755 => 100644',
475 MOD_FILENODE: 'modified file'}})]),
475 MOD_FILENODE: 'modified file'}})]),
476
476
477 # Diffs to validate rename and copy file with space in its name
477 # Diffs to validate rename and copy file with space in its name
478 ('git',
478 ('git',
479 'git_diff_rename_file_with_spaces.diff',
479 'git_diff_rename_file_with_spaces.diff',
480 [('file_with_ two spaces.txt', 'M',
480 [('file_with_ two spaces.txt', 'M',
481 {'added': 0,
481 {'added': 0,
482 'deleted': 0,
482 'deleted': 0,
483 'binary': True,
483 'binary': True,
484 'ops': {
484 'ops': {
485 RENAMED_FILENODE: (
485 RENAMED_FILENODE: (
486 'file renamed from file_with_ spaces.txt to file_with_ '
486 'file renamed from file_with_ spaces.txt to file_with_ '
487 ' two spaces.txt')}
487 ' two spaces.txt')}
488 }), ]),
488 }), ]),
489 ('hg',
489 ('hg',
490 'hg_diff_rename_file_with_spaces.diff',
490 'hg_diff_rename_file_with_spaces.diff',
491 [('file_changed _.txt', 'M',
491 [('file_changed _.txt', 'M',
492 {'added': 0,
492 {'added': 0,
493 'deleted': 0,
493 'deleted': 0,
494 'binary': True,
494 'binary': True,
495 'ops': {
495 'ops': {
496 RENAMED_FILENODE: (
496 RENAMED_FILENODE: (
497 'file renamed from file_ with update.txt to file_changed'
497 'file renamed from file_ with update.txt to file_changed'
498 ' _.txt')}
498 ' _.txt')}
499 }), ]),
499 }), ]),
500 ('hg',
500 ('hg',
501 'hg_diff_copy_file_with_spaces.diff',
501 'hg_diff_copy_file_with_spaces.diff',
502 [('file_copied_ with spaces.txt', 'M',
502 [('file_copied_ with spaces.txt', 'M',
503 {'added': 0,
503 {'added': 0,
504 'deleted': 0,
504 'deleted': 0,
505 'binary': True,
505 'binary': True,
506 'ops': {
506 'ops': {
507 COPIED_FILENODE: (
507 COPIED_FILENODE: (
508 'file copied from file_changed_without_spaces.txt to'
508 'file copied from file_changed_without_spaces.txt to'
509 ' file_copied_ with spaces.txt')}
509 ' file_copied_ with spaces.txt')}
510 }),
510 }),
511 ]),
511 ]),
512
512
513 # special signs from git
513 # special signs from git
514 ('git',
514 ('git',
515 'git_diff_binary_special_files.diff',
515 'git_diff_binary_special_files.diff',
516 [('css/_Icon\\r', 'A',
516 [('css/_Icon\\r', 'A',
517 {'added': 0,
517 {'added': 0,
518 'deleted': 0,
518 'deleted': 0,
519 'binary': True,
519 'binary': True,
520 'ops': {NEW_FILENODE: 'new file 100644',
520 'ops': {NEW_FILENODE: 'new file 100644',
521 BIN_FILENODE: 'binary diff hidden'}
521 BIN_FILENODE: 'binary diff hidden'}
522 }),
522 }),
523 ]),
523 ]),
524 ('git',
524 ('git',
525 'git_diff_binary_special_files_2.diff',
525 'git_diff_binary_special_files_2.diff',
526 [('css/Icon\\r', 'A',
526 [('css/Icon\\r', 'A',
527 {'added': 0,
527 {'added': 0,
528 'deleted': 0,
528 'deleted': 0,
529 'binary': True,
529 'binary': True,
530 'ops': {NEW_FILENODE: 'new file 100644', }
530 'ops': {NEW_FILENODE: 'new file 100644', }
531 }),
531 }),
532 ]),
532 ]),
533
533
534 # TODO: mikhail: do we still need this?
534 # TODO: mikhail: do we still need this?
535 # (
535 # (
536 # 'hg',
536 # 'hg',
537 # 'large_diff.diff',
537 # 'large_diff.diff',
538 # [
538 # [
539 # ('.hgignore', 'A', {
539 # ('.hgignore', 'A', {
540 # 'deleted': 0, 'binary': False, 'added': 3, 'ops': {
540 # 'deleted': 0, 'binary': False, 'added': 3, 'ops': {
541 # 1: 'new file 100644'}}),
541 # 1: 'new file 100644'}}),
542 # (
542 # (
543 # 'MANIFEST.in', 'A',
543 # 'MANIFEST.in', 'A',
544 # {'deleted': 0, 'binary': False, 'added': 3, 'ops': {
544 # {'deleted': 0, 'binary': False, 'added': 3, 'ops': {
545 # 1: 'new file 100644'}}),
545 # 1: 'new file 100644'}}),
546 # (
546 # (
547 # 'README.txt', 'A',
547 # 'README.txt', 'A',
548 # {'deleted': 0, 'binary': False, 'added': 19, 'ops': {
548 # {'deleted': 0, 'binary': False, 'added': 19, 'ops': {
549 # 1: 'new file 100644'}}),
549 # 1: 'new file 100644'}}),
550 # (
550 # (
551 # 'development.ini', 'A', {
551 # 'development.ini', 'A', {
552 # 'deleted': 0, 'binary': False, 'added': 116, 'ops': {
552 # 'deleted': 0, 'binary': False, 'added': 116, 'ops': {
553 # 1: 'new file 100644'}}),
553 # 1: 'new file 100644'}}),
554 # (
554 # (
555 # 'docs/index.txt', 'A', {
555 # 'docs/index.txt', 'A', {
556 # 'deleted': 0, 'binary': False, 'added': 19, 'ops': {
556 # 'deleted': 0, 'binary': False, 'added': 19, 'ops': {
557 # 1: 'new file 100644'}}),
557 # 1: 'new file 100644'}}),
558 # (
558 # (
559 # 'ez_setup.py', 'A', {
559 # 'ez_setup.py', 'A', {
560 # 'deleted': 0, 'binary': False, 'added': 276, 'ops': {
560 # 'deleted': 0, 'binary': False, 'added': 276, 'ops': {
561 # 1: 'new file 100644'}}),
561 # 1: 'new file 100644'}}),
562 # (
562 # (
563 # 'hgapp.py', 'A', {
563 # 'hgapp.py', 'A', {
564 # 'deleted': 0, 'binary': False, 'added': 26, 'ops': {
564 # 'deleted': 0, 'binary': False, 'added': 26, 'ops': {
565 # 1: 'new file 100644'}}),
565 # 1: 'new file 100644'}}),
566 # (
566 # (
567 # 'hgwebdir.config', 'A', {
567 # 'hgwebdir.config', 'A', {
568 # 'deleted': 0, 'binary': False, 'added': 21, 'ops': {
568 # 'deleted': 0, 'binary': False, 'added': 21, 'ops': {
569 # 1: 'new file 100644'}}),
569 # 1: 'new file 100644'}}),
570 # (
570 # (
571 # 'pylons_app.egg-info/PKG-INFO', 'A', {
571 # 'pylons_app.egg-info/PKG-INFO', 'A', {
572 # 'deleted': 0, 'binary': False, 'added': 10, 'ops': {
572 # 'deleted': 0, 'binary': False, 'added': 10, 'ops': {
573 # 1: 'new file 100644'}}),
573 # 1: 'new file 100644'}}),
574 # (
574 # (
575 # 'pylons_app.egg-info/SOURCES.txt', 'A', {
575 # 'pylons_app.egg-info/SOURCES.txt', 'A', {
576 # 'deleted': 0, 'binary': False, 'added': 33, 'ops': {
576 # 'deleted': 0, 'binary': False, 'added': 33, 'ops': {
577 # 1: 'new file 100644'}}),
577 # 1: 'new file 100644'}}),
578 # (
578 # (
579 # 'pylons_app.egg-info/dependency_links.txt', 'A', {
579 # 'pylons_app.egg-info/dependency_links.txt', 'A', {
580 # 'deleted': 0, 'binary': False, 'added': 1, 'ops': {
580 # 'deleted': 0, 'binary': False, 'added': 1, 'ops': {
581 # 1: 'new file 100644'}}),
581 # 1: 'new file 100644'}}),
582 # #TODO:
582 # #TODO:
583 # ]
583 # ]
584 # ),
584 # ),
585 ]
585 ]
586
586
587 DIFF_FIXTURES_WITH_CONTENT = [
587 DIFF_FIXTURES_WITH_CONTENT = [
588 (
588 (
589 'hg', 'hg_diff_single_file_change_newline.diff',
589 'hg', 'hg_diff_single_file_change_newline.diff',
590 [
590 [
591 (
591 (
592 'file_b', # filename
592 'file_b', # filename
593 'A', # change
593 'A', # change
594 { # stats
594 { # stats
595 'added': 1,
595 'added': 1,
596 'deleted': 0,
596 'deleted': 0,
597 'binary': False,
597 'binary': False,
598 'ops': {NEW_FILENODE: 'new file 100644', }
598 'ops': {NEW_FILENODE: 'new file 100644', }
599 },
599 },
600 '@@ -0,0 +1 @@\n+test_content b\n' # diff
600 '@@ -0,0 +1 @@\n+test_content b\n' # diff
601 ),
601 ),
602 ],
602 ],
603 ),
603 ),
604 (
604 (
605 'hg', 'hg_diff_double_file_change_newline.diff',
605 'hg', 'hg_diff_double_file_change_newline.diff',
606 [
606 [
607 (
607 (
608 'file_b', # filename
608 'file_b', # filename
609 'A', # change
609 'A', # change
610 { # stats
610 { # stats
611 'added': 1,
611 'added': 1,
612 'deleted': 0,
612 'deleted': 0,
613 'binary': False,
613 'binary': False,
614 'ops': {NEW_FILENODE: 'new file 100644', }
614 'ops': {NEW_FILENODE: 'new file 100644', }
615 },
615 },
616 '@@ -0,0 +1 @@\n+test_content b\n' # diff
616 '@@ -0,0 +1 @@\n+test_content b\n' # diff
617 ),
617 ),
618 (
618 (
619 'file_c', # filename
619 'file_c', # filename
620 'A', # change
620 'A', # change
621 { # stats
621 { # stats
622 'added': 1,
622 'added': 1,
623 'deleted': 0,
623 'deleted': 0,
624 'binary': False,
624 'binary': False,
625 'ops': {NEW_FILENODE: 'new file 100644', }
625 'ops': {NEW_FILENODE: 'new file 100644', }
626 },
626 },
627 '@@ -0,0 +1 @@\n+test_content c\n' # diff
627 '@@ -0,0 +1 @@\n+test_content c\n' # diff
628 ),
628 ),
629 ],
629 ],
630 ),
630 ),
631 (
631 (
632 'hg', 'hg_diff_double_file_change_double_newline.diff',
632 'hg', 'hg_diff_double_file_change_double_newline.diff',
633 [
633 [
634 (
634 (
635 'file_b', # filename
635 'file_b', # filename
636 'A', # change
636 'A', # change
637 { # stats
637 { # stats
638 'added': 1,
638 'added': 1,
639 'deleted': 0,
639 'deleted': 0,
640 'binary': False,
640 'binary': False,
641 'ops': {NEW_FILENODE: 'new file 100644', }
641 'ops': {NEW_FILENODE: 'new file 100644', }
642 },
642 },
643 '@@ -0,0 +1 @@\n+test_content b\n\n' # diff
643 '@@ -0,0 +1 @@\n+test_content b\n\n' # diff
644 ),
644 ),
645 (
645 (
646 'file_c', # filename
646 'file_c', # filename
647 'A', # change
647 'A', # change
648 { # stats
648 { # stats
649 'added': 1,
649 'added': 1,
650 'deleted': 0,
650 'deleted': 0,
651 'binary': False,
651 'binary': False,
652 'ops': {NEW_FILENODE: 'new file 100644', }
652 'ops': {NEW_FILENODE: 'new file 100644', }
653 },
653 },
654 '@@ -0,0 +1 @@\n+test_content c\n' # diff
654 '@@ -0,0 +1 @@\n+test_content c\n' # diff
655 ),
655 ),
656 ],
656 ],
657 ),
657 ),
658 (
658 (
659 'hg', 'hg_diff_four_file_change_newline.diff',
659 'hg', 'hg_diff_four_file_change_newline.diff',
660 [
660 [
661 (
661 (
662 'file', # filename
662 'file', # filename
663 'A', # change
663 'A', # change
664 { # stats
664 { # stats
665 'added': 1,
665 'added': 1,
666 'deleted': 0,
666 'deleted': 0,
667 'binary': False,
667 'binary': False,
668 'ops': {NEW_FILENODE: 'new file 100644', }
668 'ops': {NEW_FILENODE: 'new file 100644', }
669 },
669 },
670 '@@ -0,0 +1,1 @@\n+file\n' # diff
670 '@@ -0,0 +1,1 @@\n+file\n' # diff
671 ),
671 ),
672 (
672 (
673 'file2', # filename
673 'file2', # filename
674 'A', # change
674 'A', # change
675 { # stats
675 { # stats
676 'added': 1,
676 'added': 1,
677 'deleted': 0,
677 'deleted': 0,
678 'binary': False,
678 'binary': False,
679 'ops': {NEW_FILENODE: 'new file 100644', }
679 'ops': {NEW_FILENODE: 'new file 100644', }
680 },
680 },
681 '@@ -0,0 +1,1 @@\n+another line\n' # diff
681 '@@ -0,0 +1,1 @@\n+another line\n' # diff
682 ),
682 ),
683 (
683 (
684 'file3', # filename
684 'file3', # filename
685 'A', # change
685 'A', # change
686 { # stats
686 { # stats
687 'added': 1,
687 'added': 1,
688 'deleted': 0,
688 'deleted': 0,
689 'binary': False,
689 'binary': False,
690 'ops': {NEW_FILENODE: 'new file 100644', }
690 'ops': {NEW_FILENODE: 'new file 100644', }
691 },
691 },
692 '@@ -0,0 +1,1 @@\n+newline\n' # diff
692 '@@ -0,0 +1,1 @@\n+newline\n' # diff
693 ),
693 ),
694 (
694 (
695 'file4', # filename
695 'file4', # filename
696 'A', # change
696 'A', # change
697 { # stats
697 { # stats
698 'added': 1,
698 'added': 1,
699 'deleted': 0,
699 'deleted': 0,
700 'binary': False,
700 'binary': False,
701 'ops': {NEW_FILENODE: 'new file 100644', }
701 'ops': {NEW_FILENODE: 'new file 100644', }
702 },
702 },
703 '@@ -0,0 +1,1 @@\n+fil4\n\\ No newline at end of file' # diff
703 '@@ -0,0 +1,1 @@\n+fil4\n\\ No newline at end of file' # diff
704 ),
704 ),
705 ],
705 ],
706 ),
706 ),
707
707
708 ]
708 ]
709
709
710
710
711 diff_class = {
711 diff_class = {
712 'git': GitDiff,
712 'git': GitDiff,
713 'hg': MercurialDiff,
713 'hg': MercurialDiff,
714 'svn': SubversionDiff,
714 'svn': SubversionDiff,
715 }
715 }
716
716
717
717
718 @pytest.fixture(params=DIFF_FIXTURES)
718 @pytest.fixture(params=DIFF_FIXTURES)
719 def diff_fixture(request):
719 def diff_fixture(request):
720 vcs, diff_fixture, expected = request.param
720 vcs, diff_fixture, expected = request.param
721 diff_txt = fixture.load_resource(diff_fixture)
721 diff_txt = fixture.load_resource(diff_fixture)
722 diff = diff_class[vcs](diff_txt)
722 diff = diff_class[vcs](diff_txt)
723 return diff, expected
723 return diff, expected
724
724
725
725
726 def test_diff_lib(diff_fixture):
726 def test_diff_lib(diff_fixture):
727 diff, expected_data = diff_fixture
727 diff, expected_data = diff_fixture
728 diff_proc = DiffProcessor(diff)
728 diff_proc = DiffProcessor(diff)
729 diff_proc_d = diff_proc.prepare()
729 diff_proc_d = diff_proc.prepare()
730 data = [(x['filename'], x['operation'], x['stats']) for x in diff_proc_d]
730 data = [(x['filename'], x['operation'], x['stats']) for x in diff_proc_d]
731 assert expected_data == data
731 assert expected_data == data
732
732
733
733
734 @pytest.fixture(params=DIFF_FIXTURES_WITH_CONTENT)
734 @pytest.fixture(params=DIFF_FIXTURES_WITH_CONTENT)
735 def diff_fixture_w_content(request):
735 def diff_fixture_w_content(request):
736 vcs, diff_fixture, expected = request.param
736 vcs, diff_fixture, expected = request.param
737 diff_txt = fixture.load_resource(diff_fixture)
737 diff_txt = fixture.load_resource(diff_fixture)
738 diff = diff_class[vcs](diff_txt)
738 diff = diff_class[vcs](diff_txt)
739 return diff, expected
739 return diff, expected
740
740
741
741
742 def test_diff_lib_newlines(diff_fixture_w_content):
742 def test_diff_lib_newlines(diff_fixture_w_content):
743 diff, expected_data = diff_fixture_w_content
743 diff, expected_data = diff_fixture_w_content
744 diff_proc = DiffProcessor(diff)
744 diff_proc = DiffProcessor(diff)
745 diff_proc_d = diff_proc.prepare()
745 diff_proc_d = diff_proc.prepare()
746 data = [(x['filename'], x['operation'], x['stats'], x['raw_diff'])
746 data = [(x['filename'], x['operation'], x['stats'], x['raw_diff'])
747 for x in diff_proc_d]
747 for x in diff_proc_d]
748 assert expected_data == data
748 assert expected_data == data
General Comments 0
You need to be logged in to leave comments. Login now