##// END OF EJS Templates
fixes #567 diff non-unicode encoding missmatch...
marcink -
r2843:ab75def5 beta
parent child Browse files
Show More
@@ -1,636 +1,637
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import markupsafe
30 import markupsafe
31
31
32 from itertools import tee, imap
32 from itertools import tee, imap
33
33
34 from mercurial import patch
34 from mercurial import patch
35 from mercurial.mdiff import diffopts
35 from mercurial.mdiff import diffopts
36 from mercurial.bundlerepo import bundlerepository
36 from mercurial.bundlerepo import bundlerepository
37
37
38 from pylons.i18n.translation import _
38 from pylons.i18n.translation import _
39
39
40 from rhodecode.lib.compat import BytesIO
40 from rhodecode.lib.compat import BytesIO
41 from rhodecode.lib.vcs.utils.hgcompat import localrepo
41 from rhodecode.lib.vcs.utils.hgcompat import localrepo
42 from rhodecode.lib.vcs.exceptions import VCSError
42 from rhodecode.lib.vcs.exceptions import VCSError
43 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
43 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 from rhodecode.lib.helpers import escape
45 from rhodecode.lib.helpers import escape
46 from rhodecode.lib.utils import make_ui
46 from rhodecode.lib.utils import make_ui
47 from rhodecode.lib.utils2 import safe_unicode
47
48
48
49
49 def wrap_to_table(str_):
50 def wrap_to_table(str_):
50 return '''<table class="code-difftable">
51 return '''<table class="code-difftable">
51 <tr class="line no-comment">
52 <tr class="line no-comment">
52 <td class="lineno new"></td>
53 <td class="lineno new"></td>
53 <td class="code no-comment"><pre>%s</pre></td>
54 <td class="code no-comment"><pre>%s</pre></td>
54 </tr>
55 </tr>
55 </table>''' % str_
56 </table>''' % str_
56
57
57
58
58 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
59 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
59 ignore_whitespace=True, line_context=3,
60 ignore_whitespace=True, line_context=3,
60 enable_comments=False):
61 enable_comments=False):
61 """
62 """
62 returns a wrapped diff into a table, checks for cut_off_limit and presents
63 returns a wrapped diff into a table, checks for cut_off_limit and presents
63 proper message
64 proper message
64 """
65 """
65
66
66 if filenode_old is None:
67 if filenode_old is None:
67 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
68 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
68
69
69 if filenode_old.is_binary or filenode_new.is_binary:
70 if filenode_old.is_binary or filenode_new.is_binary:
70 diff = wrap_to_table(_('binary file'))
71 diff = wrap_to_table(_('binary file'))
71 stats = (0, 0)
72 stats = (0, 0)
72 size = 0
73 size = 0
73
74
74 elif cut_off_limit != -1 and (cut_off_limit is None or
75 elif cut_off_limit != -1 and (cut_off_limit is None or
75 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
76 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
76
77
77 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
78 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
78 ignore_whitespace=ignore_whitespace,
79 ignore_whitespace=ignore_whitespace,
79 context=line_context)
80 context=line_context)
80 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
81 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
81
82
82 diff = diff_processor.as_html(enable_comments=enable_comments)
83 diff = diff_processor.as_html(enable_comments=enable_comments)
83 stats = diff_processor.stat()
84 stats = diff_processor.stat()
84 size = len(diff or '')
85 size = len(diff or '')
85 else:
86 else:
86 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
87 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
87 'diff menu to display this diff'))
88 'diff menu to display this diff'))
88 stats = (0, 0)
89 stats = (0, 0)
89 size = 0
90 size = 0
90 if not diff:
91 if not diff:
91 submodules = filter(lambda o: isinstance(o, SubModuleNode),
92 submodules = filter(lambda o: isinstance(o, SubModuleNode),
92 [filenode_new, filenode_old])
93 [filenode_new, filenode_old])
93 if submodules:
94 if submodules:
94 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
95 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
95 else:
96 else:
96 diff = wrap_to_table(_('No changes detected'))
97 diff = wrap_to_table(_('No changes detected'))
97
98
98 cs1 = filenode_old.changeset.raw_id
99 cs1 = filenode_old.changeset.raw_id
99 cs2 = filenode_new.changeset.raw_id
100 cs2 = filenode_new.changeset.raw_id
100
101
101 return size, cs1, cs2, diff, stats
102 return size, cs1, cs2, diff, stats
102
103
103
104
104 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
105 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
105 """
106 """
106 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
107 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
107
108
108 :param ignore_whitespace: ignore whitespaces in diff
109 :param ignore_whitespace: ignore whitespaces in diff
109 """
110 """
110 # make sure we pass in default context
111 # make sure we pass in default context
111 context = context or 3
112 context = context or 3
112 submodules = filter(lambda o: isinstance(o, SubModuleNode),
113 submodules = filter(lambda o: isinstance(o, SubModuleNode),
113 [filenode_new, filenode_old])
114 [filenode_new, filenode_old])
114 if submodules:
115 if submodules:
115 return ''
116 return ''
116
117
117 for filenode in (filenode_old, filenode_new):
118 for filenode in (filenode_old, filenode_new):
118 if not isinstance(filenode, FileNode):
119 if not isinstance(filenode, FileNode):
119 raise VCSError("Given object should be FileNode object, not %s"
120 raise VCSError("Given object should be FileNode object, not %s"
120 % filenode.__class__)
121 % filenode.__class__)
121
122
122 repo = filenode_new.changeset.repository
123 repo = filenode_new.changeset.repository
123 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
124 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
124 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
125 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
125
126
126 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
127 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
127 ignore_whitespace, context)
128 ignore_whitespace, context)
128 return vcs_gitdiff
129 return vcs_gitdiff
129
130
130
131
131 class DiffProcessor(object):
132 class DiffProcessor(object):
132 """
133 """
133 Give it a unified diff and it returns a list of the files that were
134 Give it a unified diff and it returns a list of the files that were
134 mentioned in the diff together with a dict of meta information that
135 mentioned in the diff together with a dict of meta information that
135 can be used to render it in a HTML template.
136 can be used to render it in a HTML template.
136 """
137 """
137 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
138 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
138 _newline_marker = '\\ No newline at end of file\n'
139 _newline_marker = '\\ No newline at end of file\n'
139
140
140 def __init__(self, diff, differ='diff', format='gitdiff'):
141 def __init__(self, diff, differ='diff', format='gitdiff'):
141 """
142 """
142 :param diff: a text in diff format or generator
143 :param diff: a text in diff format or generator
143 :param format: format of diff passed, `udiff` or `gitdiff`
144 :param format: format of diff passed, `udiff` or `gitdiff`
144 """
145 """
145 if isinstance(diff, basestring):
146 if isinstance(diff, basestring):
146 diff = [diff]
147 diff = [diff]
147
148
148 self.__udiff = diff
149 self.__udiff = diff
149 self.__format = format
150 self.__format = format
150 self.adds = 0
151 self.adds = 0
151 self.removes = 0
152 self.removes = 0
152
153
153 if isinstance(self.__udiff, basestring):
154 if isinstance(self.__udiff, basestring):
154 self.lines = iter(self.__udiff.splitlines(1))
155 self.lines = iter(self.__udiff.splitlines(1))
155
156
156 elif self.__format == 'gitdiff':
157 elif self.__format == 'gitdiff':
157 udiff_copy = self.copy_iterator()
158 udiff_copy = self.copy_iterator()
158 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
159 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
159 else:
160 else:
160 udiff_copy = self.copy_iterator()
161 udiff_copy = self.copy_iterator()
161 self.lines = imap(self.escaper, udiff_copy)
162 self.lines = imap(self.escaper, udiff_copy)
162
163
163 # Select a differ.
164 # Select a differ.
164 if differ == 'difflib':
165 if differ == 'difflib':
165 self.differ = self._highlight_line_difflib
166 self.differ = self._highlight_line_difflib
166 else:
167 else:
167 self.differ = self._highlight_line_udiff
168 self.differ = self._highlight_line_udiff
168
169
169 def escaper(self, string):
170 def escaper(self, string):
170 return markupsafe.escape(string)
171 return markupsafe.escape(string)
171
172
172 def copy_iterator(self):
173 def copy_iterator(self):
173 """
174 """
174 make a fresh copy of generator, we should not iterate thru
175 make a fresh copy of generator, we should not iterate thru
175 an original as it's needed for repeating operations on
176 an original as it's needed for repeating operations on
176 this instance of DiffProcessor
177 this instance of DiffProcessor
177 """
178 """
178 self.__udiff, iterator_copy = tee(self.__udiff)
179 self.__udiff, iterator_copy = tee(self.__udiff)
179 return iterator_copy
180 return iterator_copy
180
181
181 def _extract_rev(self, line1, line2):
182 def _extract_rev(self, line1, line2):
182 """
183 """
183 Extract the operation (A/M/D), filename and revision hint from a line.
184 Extract the operation (A/M/D), filename and revision hint from a line.
184 """
185 """
185
186
186 try:
187 try:
187 if line1.startswith('--- ') and line2.startswith('+++ '):
188 if line1.startswith('--- ') and line2.startswith('+++ '):
188 l1 = line1[4:].split(None, 1)
189 l1 = line1[4:].split(None, 1)
189 old_filename = (l1[0].replace('a/', '', 1)
190 old_filename = (l1[0].replace('a/', '', 1)
190 if len(l1) >= 1 else None)
191 if len(l1) >= 1 else None)
191 old_rev = l1[1] if len(l1) == 2 else 'old'
192 old_rev = l1[1] if len(l1) == 2 else 'old'
192
193
193 l2 = line2[4:].split(None, 1)
194 l2 = line2[4:].split(None, 1)
194 new_filename = (l2[0].replace('b/', '', 1)
195 new_filename = (l2[0].replace('b/', '', 1)
195 if len(l1) >= 1 else None)
196 if len(l1) >= 1 else None)
196 new_rev = l2[1] if len(l2) == 2 else 'new'
197 new_rev = l2[1] if len(l2) == 2 else 'new'
197
198
198 filename = (old_filename
199 filename = (old_filename
199 if old_filename != '/dev/null' else new_filename)
200 if old_filename != '/dev/null' else new_filename)
200
201
201 operation = 'D' if new_filename == '/dev/null' else None
202 operation = 'D' if new_filename == '/dev/null' else None
202 if not operation:
203 if not operation:
203 operation = 'M' if old_filename != '/dev/null' else 'A'
204 operation = 'M' if old_filename != '/dev/null' else 'A'
204
205
205 return operation, filename, new_rev, old_rev
206 return operation, filename, new_rev, old_rev
206 except (ValueError, IndexError):
207 except (ValueError, IndexError):
207 pass
208 pass
208
209
209 return None, None, None, None
210 return None, None, None, None
210
211
211 def _parse_gitdiff(self, diffiterator):
212 def _parse_gitdiff(self, diffiterator):
212 def line_decoder(l):
213 def line_decoder(l):
213 if l.startswith('+') and not l.startswith('+++'):
214 if l.startswith('+') and not l.startswith('+++'):
214 self.adds += 1
215 self.adds += 1
215 elif l.startswith('-') and not l.startswith('---'):
216 elif l.startswith('-') and not l.startswith('---'):
216 self.removes += 1
217 self.removes += 1
217 return l.decode('utf8', 'replace')
218 return safe_unicode(l)
218
219
219 output = list(diffiterator)
220 output = list(diffiterator)
220 size = len(output)
221 size = len(output)
221
222
222 if size == 2:
223 if size == 2:
223 l = []
224 l = []
224 l.extend([output[0]])
225 l.extend([output[0]])
225 l.extend(output[1].splitlines(1))
226 l.extend(output[1].splitlines(1))
226 return map(line_decoder, l)
227 return map(line_decoder, l)
227 elif size == 1:
228 elif size == 1:
228 return map(line_decoder, output[0].splitlines(1))
229 return map(line_decoder, output[0].splitlines(1))
229 elif size == 0:
230 elif size == 0:
230 return []
231 return []
231
232
232 raise Exception('wrong size of diff %s' % size)
233 raise Exception('wrong size of diff %s' % size)
233
234
234 def _highlight_line_difflib(self, line, next_):
235 def _highlight_line_difflib(self, line, next_):
235 """
236 """
236 Highlight inline changes in both lines.
237 Highlight inline changes in both lines.
237 """
238 """
238
239
239 if line['action'] == 'del':
240 if line['action'] == 'del':
240 old, new = line, next_
241 old, new = line, next_
241 else:
242 else:
242 old, new = next_, line
243 old, new = next_, line
243
244
244 oldwords = re.split(r'(\W)', old['line'])
245 oldwords = re.split(r'(\W)', old['line'])
245 newwords = re.split(r'(\W)', new['line'])
246 newwords = re.split(r'(\W)', new['line'])
246
247
247 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
248 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
248
249
249 oldfragments, newfragments = [], []
250 oldfragments, newfragments = [], []
250 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
251 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
251 oldfrag = ''.join(oldwords[i1:i2])
252 oldfrag = ''.join(oldwords[i1:i2])
252 newfrag = ''.join(newwords[j1:j2])
253 newfrag = ''.join(newwords[j1:j2])
253 if tag != 'equal':
254 if tag != 'equal':
254 if oldfrag:
255 if oldfrag:
255 oldfrag = '<del>%s</del>' % oldfrag
256 oldfrag = '<del>%s</del>' % oldfrag
256 if newfrag:
257 if newfrag:
257 newfrag = '<ins>%s</ins>' % newfrag
258 newfrag = '<ins>%s</ins>' % newfrag
258 oldfragments.append(oldfrag)
259 oldfragments.append(oldfrag)
259 newfragments.append(newfrag)
260 newfragments.append(newfrag)
260
261
261 old['line'] = "".join(oldfragments)
262 old['line'] = "".join(oldfragments)
262 new['line'] = "".join(newfragments)
263 new['line'] = "".join(newfragments)
263
264
264 def _highlight_line_udiff(self, line, next_):
265 def _highlight_line_udiff(self, line, next_):
265 """
266 """
266 Highlight inline changes in both lines.
267 Highlight inline changes in both lines.
267 """
268 """
268 start = 0
269 start = 0
269 limit = min(len(line['line']), len(next_['line']))
270 limit = min(len(line['line']), len(next_['line']))
270 while start < limit and line['line'][start] == next_['line'][start]:
271 while start < limit and line['line'][start] == next_['line'][start]:
271 start += 1
272 start += 1
272 end = -1
273 end = -1
273 limit -= start
274 limit -= start
274 while -end <= limit and line['line'][end] == next_['line'][end]:
275 while -end <= limit and line['line'][end] == next_['line'][end]:
275 end -= 1
276 end -= 1
276 end += 1
277 end += 1
277 if start or end:
278 if start or end:
278 def do(l):
279 def do(l):
279 last = end + len(l['line'])
280 last = end + len(l['line'])
280 if l['action'] == 'add':
281 if l['action'] == 'add':
281 tag = 'ins'
282 tag = 'ins'
282 else:
283 else:
283 tag = 'del'
284 tag = 'del'
284 l['line'] = '%s<%s>%s</%s>%s' % (
285 l['line'] = '%s<%s>%s</%s>%s' % (
285 l['line'][:start],
286 l['line'][:start],
286 tag,
287 tag,
287 l['line'][start:last],
288 l['line'][start:last],
288 tag,
289 tag,
289 l['line'][last:]
290 l['line'][last:]
290 )
291 )
291 do(line)
292 do(line)
292 do(next_)
293 do(next_)
293
294
294 def _parse_udiff(self, inline_diff=True):
295 def _parse_udiff(self, inline_diff=True):
295 """
296 """
296 Parse the diff an return data for the template.
297 Parse the diff an return data for the template.
297 """
298 """
298 lineiter = self.lines
299 lineiter = self.lines
299 files = []
300 files = []
300 try:
301 try:
301 line = lineiter.next()
302 line = lineiter.next()
302 while 1:
303 while 1:
303 # continue until we found the old file
304 # continue until we found the old file
304 if not line.startswith('--- '):
305 if not line.startswith('--- '):
305 line = lineiter.next()
306 line = lineiter.next()
306 continue
307 continue
307
308
308 chunks = []
309 chunks = []
309 stats = [0, 0]
310 stats = [0, 0]
310 operation, filename, old_rev, new_rev = \
311 operation, filename, old_rev, new_rev = \
311 self._extract_rev(line, lineiter.next())
312 self._extract_rev(line, lineiter.next())
312 files.append({
313 files.append({
313 'filename': filename,
314 'filename': filename,
314 'old_revision': old_rev,
315 'old_revision': old_rev,
315 'new_revision': new_rev,
316 'new_revision': new_rev,
316 'chunks': chunks,
317 'chunks': chunks,
317 'operation': operation,
318 'operation': operation,
318 'stats': stats,
319 'stats': stats,
319 })
320 })
320
321
321 line = lineiter.next()
322 line = lineiter.next()
322 while line:
323 while line:
323 match = self._chunk_re.match(line)
324 match = self._chunk_re.match(line)
324 if not match:
325 if not match:
325 break
326 break
326
327
327 lines = []
328 lines = []
328 chunks.append(lines)
329 chunks.append(lines)
329
330
330 old_line, old_end, new_line, new_end = \
331 old_line, old_end, new_line, new_end = \
331 [int(x or 1) for x in match.groups()[:-1]]
332 [int(x or 1) for x in match.groups()[:-1]]
332 old_line -= 1
333 old_line -= 1
333 new_line -= 1
334 new_line -= 1
334 gr = match.groups()
335 gr = match.groups()
335 context = len(gr) == 5
336 context = len(gr) == 5
336 old_end += old_line
337 old_end += old_line
337 new_end += new_line
338 new_end += new_line
338
339
339 if context:
340 if context:
340 # skip context only if it's first line
341 # skip context only if it's first line
341 if int(gr[0]) > 1:
342 if int(gr[0]) > 1:
342 lines.append({
343 lines.append({
343 'old_lineno': '...',
344 'old_lineno': '...',
344 'new_lineno': '...',
345 'new_lineno': '...',
345 'action': 'context',
346 'action': 'context',
346 'line': line,
347 'line': line,
347 })
348 })
348
349
349 line = lineiter.next()
350 line = lineiter.next()
350
351
351 while old_line < old_end or new_line < new_end:
352 while old_line < old_end or new_line < new_end:
352 if line:
353 if line:
353 command = line[0]
354 command = line[0]
354 if command in ['+', '-', ' ']:
355 if command in ['+', '-', ' ']:
355 #only modify the line if it's actually a diff
356 #only modify the line if it's actually a diff
356 # thing
357 # thing
357 line = line[1:]
358 line = line[1:]
358 else:
359 else:
359 command = ' '
360 command = ' '
360
361
361 affects_old = affects_new = False
362 affects_old = affects_new = False
362
363
363 # ignore those if we don't expect them
364 # ignore those if we don't expect them
364 if command in '#@':
365 if command in '#@':
365 continue
366 continue
366 elif command == '+':
367 elif command == '+':
367 affects_new = True
368 affects_new = True
368 action = 'add'
369 action = 'add'
369 stats[0] += 1
370 stats[0] += 1
370 elif command == '-':
371 elif command == '-':
371 affects_old = True
372 affects_old = True
372 action = 'del'
373 action = 'del'
373 stats[1] += 1
374 stats[1] += 1
374 else:
375 else:
375 affects_old = affects_new = True
376 affects_old = affects_new = True
376 action = 'unmod'
377 action = 'unmod'
377
378
378 if line != self._newline_marker:
379 if line != self._newline_marker:
379 old_line += affects_old
380 old_line += affects_old
380 new_line += affects_new
381 new_line += affects_new
381 lines.append({
382 lines.append({
382 'old_lineno': affects_old and old_line or '',
383 'old_lineno': affects_old and old_line or '',
383 'new_lineno': affects_new and new_line or '',
384 'new_lineno': affects_new and new_line or '',
384 'action': action,
385 'action': action,
385 'line': line
386 'line': line
386 })
387 })
387
388
388 line = lineiter.next()
389 line = lineiter.next()
389 if line == self._newline_marker:
390 if line == self._newline_marker:
390 # we need to append to lines, since this is not
391 # we need to append to lines, since this is not
391 # counted in the line specs of diff
392 # counted in the line specs of diff
392 lines.append({
393 lines.append({
393 'old_lineno': '...',
394 'old_lineno': '...',
394 'new_lineno': '...',
395 'new_lineno': '...',
395 'action': 'context',
396 'action': 'context',
396 'line': line
397 'line': line
397 })
398 })
398
399
399 except StopIteration:
400 except StopIteration:
400 pass
401 pass
401
402
402 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
403 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
403 if inline_diff is False:
404 if inline_diff is False:
404 return sorted(files, key=sorter)
405 return sorted(files, key=sorter)
405
406
406 # highlight inline changes
407 # highlight inline changes
407 for diff_data in files:
408 for diff_data in files:
408 for chunk in diff_data['chunks']:
409 for chunk in diff_data['chunks']:
409 lineiter = iter(chunk)
410 lineiter = iter(chunk)
410 try:
411 try:
411 while 1:
412 while 1:
412 line = lineiter.next()
413 line = lineiter.next()
413 if line['action'] not in ['unmod', 'context']:
414 if line['action'] not in ['unmod', 'context']:
414 nextline = lineiter.next()
415 nextline = lineiter.next()
415 if nextline['action'] in ['unmod', 'context'] or \
416 if nextline['action'] in ['unmod', 'context'] or \
416 nextline['action'] == line['action']:
417 nextline['action'] == line['action']:
417 continue
418 continue
418 self.differ(line, nextline)
419 self.differ(line, nextline)
419 except StopIteration:
420 except StopIteration:
420 pass
421 pass
421
422
422 return sorted(files, key=sorter)
423 return sorted(files, key=sorter)
423
424
424 def prepare(self, inline_diff=True):
425 def prepare(self, inline_diff=True):
425 """
426 """
426 Prepare the passed udiff for HTML rendering. It'l return a list
427 Prepare the passed udiff for HTML rendering. It'l return a list
427 of dicts
428 of dicts
428 """
429 """
429 return self._parse_udiff(inline_diff=inline_diff)
430 return self._parse_udiff(inline_diff=inline_diff)
430
431
431 def _safe_id(self, idstring):
432 def _safe_id(self, idstring):
432 """Make a string safe for including in an id attribute.
433 """Make a string safe for including in an id attribute.
433
434
434 The HTML spec says that id attributes 'must begin with
435 The HTML spec says that id attributes 'must begin with
435 a letter ([A-Za-z]) and may be followed by any number
436 a letter ([A-Za-z]) and may be followed by any number
436 of letters, digits ([0-9]), hyphens ("-"), underscores
437 of letters, digits ([0-9]), hyphens ("-"), underscores
437 ("_"), colons (":"), and periods (".")'. These regexps
438 ("_"), colons (":"), and periods (".")'. These regexps
438 are slightly over-zealous, in that they remove colons
439 are slightly over-zealous, in that they remove colons
439 and periods unnecessarily.
440 and periods unnecessarily.
440
441
441 Whitespace is transformed into underscores, and then
442 Whitespace is transformed into underscores, and then
442 anything which is not a hyphen or a character that
443 anything which is not a hyphen or a character that
443 matches \w (alphanumerics and underscore) is removed.
444 matches \w (alphanumerics and underscore) is removed.
444
445
445 """
446 """
446 # Transform all whitespace to underscore
447 # Transform all whitespace to underscore
447 idstring = re.sub(r'\s', "_", '%s' % idstring)
448 idstring = re.sub(r'\s', "_", '%s' % idstring)
448 # Remove everything that is not a hyphen or a member of \w
449 # Remove everything that is not a hyphen or a member of \w
449 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
450 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
450 return idstring
451 return idstring
451
452
452 def raw_diff(self):
453 def raw_diff(self):
453 """
454 """
454 Returns raw string as udiff
455 Returns raw string as udiff
455 """
456 """
456 udiff_copy = self.copy_iterator()
457 udiff_copy = self.copy_iterator()
457 if self.__format == 'gitdiff':
458 if self.__format == 'gitdiff':
458 udiff_copy = self._parse_gitdiff(udiff_copy)
459 udiff_copy = self._parse_gitdiff(udiff_copy)
459 return u''.join(udiff_copy)
460 return u''.join(udiff_copy)
460
461
461 def as_html(self, table_class='code-difftable', line_class='line',
462 def as_html(self, table_class='code-difftable', line_class='line',
462 new_lineno_class='lineno old', old_lineno_class='lineno new',
463 new_lineno_class='lineno old', old_lineno_class='lineno new',
463 code_class='code', enable_comments=False, diff_lines=None):
464 code_class='code', enable_comments=False, diff_lines=None):
464 """
465 """
465 Return given diff as html table with customized css classes
466 Return given diff as html table with customized css classes
466 """
467 """
467 def _link_to_if(condition, label, url):
468 def _link_to_if(condition, label, url):
468 """
469 """
469 Generates a link if condition is meet or just the label if not.
470 Generates a link if condition is meet or just the label if not.
470 """
471 """
471
472
472 if condition:
473 if condition:
473 return '''<a href="%(url)s">%(label)s</a>''' % {
474 return '''<a href="%(url)s">%(label)s</a>''' % {
474 'url': url,
475 'url': url,
475 'label': label
476 'label': label
476 }
477 }
477 else:
478 else:
478 return label
479 return label
479 if diff_lines is None:
480 if diff_lines is None:
480 diff_lines = self.prepare()
481 diff_lines = self.prepare()
481 _html_empty = True
482 _html_empty = True
482 _html = []
483 _html = []
483 _html.append('''<table class="%(table_class)s">\n''' % {
484 _html.append('''<table class="%(table_class)s">\n''' % {
484 'table_class': table_class
485 'table_class': table_class
485 })
486 })
486 for diff in diff_lines:
487 for diff in diff_lines:
487 for line in diff['chunks']:
488 for line in diff['chunks']:
488 _html_empty = False
489 _html_empty = False
489 for change in line:
490 for change in line:
490 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
491 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
491 'lc': line_class,
492 'lc': line_class,
492 'action': change['action']
493 'action': change['action']
493 })
494 })
494 anchor_old_id = ''
495 anchor_old_id = ''
495 anchor_new_id = ''
496 anchor_new_id = ''
496 anchor_old = "%(filename)s_o%(oldline_no)s" % {
497 anchor_old = "%(filename)s_o%(oldline_no)s" % {
497 'filename': self._safe_id(diff['filename']),
498 'filename': self._safe_id(diff['filename']),
498 'oldline_no': change['old_lineno']
499 'oldline_no': change['old_lineno']
499 }
500 }
500 anchor_new = "%(filename)s_n%(oldline_no)s" % {
501 anchor_new = "%(filename)s_n%(oldline_no)s" % {
501 'filename': self._safe_id(diff['filename']),
502 'filename': self._safe_id(diff['filename']),
502 'oldline_no': change['new_lineno']
503 'oldline_no': change['new_lineno']
503 }
504 }
504 cond_old = (change['old_lineno'] != '...' and
505 cond_old = (change['old_lineno'] != '...' and
505 change['old_lineno'])
506 change['old_lineno'])
506 cond_new = (change['new_lineno'] != '...' and
507 cond_new = (change['new_lineno'] != '...' and
507 change['new_lineno'])
508 change['new_lineno'])
508 if cond_old:
509 if cond_old:
509 anchor_old_id = 'id="%s"' % anchor_old
510 anchor_old_id = 'id="%s"' % anchor_old
510 if cond_new:
511 if cond_new:
511 anchor_new_id = 'id="%s"' % anchor_new
512 anchor_new_id = 'id="%s"' % anchor_new
512 ###########################################################
513 ###########################################################
513 # OLD LINE NUMBER
514 # OLD LINE NUMBER
514 ###########################################################
515 ###########################################################
515 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
516 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
516 'a_id': anchor_old_id,
517 'a_id': anchor_old_id,
517 'olc': old_lineno_class
518 'olc': old_lineno_class
518 })
519 })
519
520
520 _html.append('''%(link)s''' % {
521 _html.append('''%(link)s''' % {
521 'link': _link_to_if(True, change['old_lineno'],
522 'link': _link_to_if(True, change['old_lineno'],
522 '#%s' % anchor_old)
523 '#%s' % anchor_old)
523 })
524 })
524 _html.append('''</td>\n''')
525 _html.append('''</td>\n''')
525 ###########################################################
526 ###########################################################
526 # NEW LINE NUMBER
527 # NEW LINE NUMBER
527 ###########################################################
528 ###########################################################
528
529
529 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
530 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
530 'a_id': anchor_new_id,
531 'a_id': anchor_new_id,
531 'nlc': new_lineno_class
532 'nlc': new_lineno_class
532 })
533 })
533
534
534 _html.append('''%(link)s''' % {
535 _html.append('''%(link)s''' % {
535 'link': _link_to_if(True, change['new_lineno'],
536 'link': _link_to_if(True, change['new_lineno'],
536 '#%s' % anchor_new)
537 '#%s' % anchor_new)
537 })
538 })
538 _html.append('''</td>\n''')
539 _html.append('''</td>\n''')
539 ###########################################################
540 ###########################################################
540 # CODE
541 # CODE
541 ###########################################################
542 ###########################################################
542 comments = '' if enable_comments else 'no-comment'
543 comments = '' if enable_comments else 'no-comment'
543 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
544 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
544 'cc': code_class,
545 'cc': code_class,
545 'inc': comments
546 'inc': comments
546 })
547 })
547 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
548 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
548 'code': change['line']
549 'code': change['line']
549 })
550 })
550 _html.append('''\t</td>''')
551 _html.append('''\t</td>''')
551 _html.append('''\n</tr>\n''')
552 _html.append('''\n</tr>\n''')
552 _html.append('''</table>''')
553 _html.append('''</table>''')
553 if _html_empty:
554 if _html_empty:
554 return None
555 return None
555 return ''.join(_html)
556 return ''.join(_html)
556
557
557 def stat(self):
558 def stat(self):
558 """
559 """
559 Returns tuple of added, and removed lines for this instance
560 Returns tuple of added, and removed lines for this instance
560 """
561 """
561 return self.adds, self.removes
562 return self.adds, self.removes
562
563
563
564
564 class InMemoryBundleRepo(bundlerepository):
565 class InMemoryBundleRepo(bundlerepository):
565 def __init__(self, ui, path, bundlestream):
566 def __init__(self, ui, path, bundlestream):
566 self._tempparent = None
567 self._tempparent = None
567 localrepo.localrepository.__init__(self, ui, path)
568 localrepo.localrepository.__init__(self, ui, path)
568 self.ui.setconfig('phases', 'publish', False)
569 self.ui.setconfig('phases', 'publish', False)
569
570
570 self.bundle = bundlestream
571 self.bundle = bundlestream
571
572
572 # dict with the mapping 'filename' -> position in the bundle
573 # dict with the mapping 'filename' -> position in the bundle
573 self.bundlefilespos = {}
574 self.bundlefilespos = {}
574
575
575
576
576 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None):
577 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None):
577 """
578 """
578 General differ between branches, bookmarks or separate but releated
579 General differ between branches, bookmarks or separate but releated
579 repositories
580 repositories
580
581
581 :param org_repo:
582 :param org_repo:
582 :type org_repo:
583 :type org_repo:
583 :param org_ref:
584 :param org_ref:
584 :type org_ref:
585 :type org_ref:
585 :param other_repo:
586 :param other_repo:
586 :type other_repo:
587 :type other_repo:
587 :param other_ref:
588 :param other_ref:
588 :type other_ref:
589 :type other_ref:
589 """
590 """
590
591
591 bundlerepo = None
592 bundlerepo = None
592 ignore_whitespace = False
593 ignore_whitespace = False
593 context = 3
594 context = 3
594 org_repo = org_repo.scm_instance._repo
595 org_repo = org_repo.scm_instance._repo
595 other_repo = other_repo.scm_instance._repo
596 other_repo = other_repo.scm_instance._repo
596 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
597 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
597 org_ref = org_ref[1]
598 org_ref = org_ref[1]
598 other_ref = other_ref[1]
599 other_ref = other_ref[1]
599
600
600 if org_repo != other_repo:
601 if org_repo != other_repo:
601
602
602 common, incoming, rheads = discovery_data
603 common, incoming, rheads = discovery_data
603 other_repo_peer = localrepo.locallegacypeer(other_repo.local())
604 other_repo_peer = localrepo.locallegacypeer(other_repo.local())
604 # create a bundle (uncompressed if other repo is not local)
605 # create a bundle (uncompressed if other repo is not local)
605 if other_repo_peer.capable('getbundle') and incoming:
606 if other_repo_peer.capable('getbundle') and incoming:
606 # disable repo hooks here since it's just bundle !
607 # disable repo hooks here since it's just bundle !
607 # patch and reset hooks section of UI config to not run any
608 # patch and reset hooks section of UI config to not run any
608 # hooks on fetching archives with subrepos
609 # hooks on fetching archives with subrepos
609 for k, _ in other_repo.ui.configitems('hooks'):
610 for k, _ in other_repo.ui.configitems('hooks'):
610 other_repo.ui.setconfig('hooks', k, None)
611 other_repo.ui.setconfig('hooks', k, None)
611
612
612 unbundle = other_repo.getbundle('incoming', common=common,
613 unbundle = other_repo.getbundle('incoming', common=common,
613 heads=None)
614 heads=None)
614
615
615 buf = BytesIO()
616 buf = BytesIO()
616 while True:
617 while True:
617 chunk = unbundle._stream.read(1024 * 4)
618 chunk = unbundle._stream.read(1024 * 4)
618 if not chunk:
619 if not chunk:
619 break
620 break
620 buf.write(chunk)
621 buf.write(chunk)
621
622
622 buf.seek(0)
623 buf.seek(0)
623 # replace chunked _stream with data that can do tell() and seek()
624 # replace chunked _stream with data that can do tell() and seek()
624 unbundle._stream = buf
625 unbundle._stream = buf
625
626
626 ui = make_ui('db')
627 ui = make_ui('db')
627 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
628 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
628 bundlestream=unbundle)
629 bundlestream=unbundle)
629
630
630 return ''.join(patch.diff(bundlerepo or org_repo,
631 return ''.join(patch.diff(bundlerepo or org_repo,
631 node1=org_repo[org_ref].node(),
632 node1=org_repo[org_ref].node(),
632 node2=other_repo[other_ref].node(),
633 node2=other_repo[other_ref].node(),
633 opts=opts))
634 opts=opts))
634 else:
635 else:
635 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
636 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
636 opts=opts))
637 opts=opts))
@@ -1,460 +1,461
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # original copyright: 2007-2008 by Armin Ronacher
2 # original copyright: 2007-2008 by Armin Ronacher
3 # licensed under the BSD license.
3 # licensed under the BSD license.
4
4
5 import re
5 import re
6 import difflib
6 import difflib
7 import logging
7 import logging
8
8
9 from difflib import unified_diff
9 from difflib import unified_diff
10 from itertools import tee, imap
10 from itertools import tee, imap
11
11
12 from mercurial.match import match
12 from mercurial.match import match
13
13
14 from rhodecode.lib.vcs.exceptions import VCSError
14 from rhodecode.lib.vcs.exceptions import VCSError
15 from rhodecode.lib.vcs.nodes import FileNode, NodeError
15 from rhodecode.lib.vcs.nodes import FileNode, NodeError
16 from rhodecode.lib.vcs.utils import safe_unicode
16
17
17
18
18 def get_udiff(filenode_old, filenode_new, show_whitespace=True):
19 def get_udiff(filenode_old, filenode_new, show_whitespace=True):
19 """
20 """
20 Returns unified diff between given ``filenode_old`` and ``filenode_new``.
21 Returns unified diff between given ``filenode_old`` and ``filenode_new``.
21 """
22 """
22 try:
23 try:
23 filenode_old_date = filenode_old.changeset.date
24 filenode_old_date = filenode_old.changeset.date
24 except NodeError:
25 except NodeError:
25 filenode_old_date = None
26 filenode_old_date = None
26
27
27 try:
28 try:
28 filenode_new_date = filenode_new.changeset.date
29 filenode_new_date = filenode_new.changeset.date
29 except NodeError:
30 except NodeError:
30 filenode_new_date = None
31 filenode_new_date = None
31
32
32 for filenode in (filenode_old, filenode_new):
33 for filenode in (filenode_old, filenode_new):
33 if not isinstance(filenode, FileNode):
34 if not isinstance(filenode, FileNode):
34 raise VCSError("Given object should be FileNode object, not %s"
35 raise VCSError("Given object should be FileNode object, not %s"
35 % filenode.__class__)
36 % filenode.__class__)
36
37
37 if filenode_old_date and filenode_new_date:
38 if filenode_old_date and filenode_new_date:
38 if not filenode_old_date < filenode_new_date:
39 if not filenode_old_date < filenode_new_date:
39 logging.debug("Generating udiff for filenodes with not increasing "
40 logging.debug("Generating udiff for filenodes with not increasing "
40 "dates")
41 "dates")
41
42
42 vcs_udiff = unified_diff(filenode_old.content.splitlines(True),
43 vcs_udiff = unified_diff(filenode_old.content.splitlines(True),
43 filenode_new.content.splitlines(True),
44 filenode_new.content.splitlines(True),
44 filenode_old.name,
45 filenode_old.name,
45 filenode_new.name,
46 filenode_new.name,
46 filenode_old_date,
47 filenode_old_date,
47 filenode_old_date)
48 filenode_old_date)
48 return vcs_udiff
49 return vcs_udiff
49
50
50
51
51 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True):
52 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True):
52 """
53 """
53 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
54 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
54
55
55 :param ignore_whitespace: ignore whitespaces in diff
56 :param ignore_whitespace: ignore whitespaces in diff
56 """
57 """
57
58
58 for filenode in (filenode_old, filenode_new):
59 for filenode in (filenode_old, filenode_new):
59 if not isinstance(filenode, FileNode):
60 if not isinstance(filenode, FileNode):
60 raise VCSError("Given object should be FileNode object, not %s"
61 raise VCSError("Given object should be FileNode object, not %s"
61 % filenode.__class__)
62 % filenode.__class__)
62
63
63 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
64 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
64 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
65 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
65
66
66 repo = filenode_new.changeset.repository
67 repo = filenode_new.changeset.repository
67 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
68 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
68 ignore_whitespace)
69 ignore_whitespace)
69
70
70 return vcs_gitdiff
71 return vcs_gitdiff
71
72
72
73
73 class DiffProcessor(object):
74 class DiffProcessor(object):
74 """
75 """
75 Give it a unified diff and it returns a list of the files that were
76 Give it a unified diff and it returns a list of the files that were
76 mentioned in the diff together with a dict of meta information that
77 mentioned in the diff together with a dict of meta information that
77 can be used to render it in a HTML template.
78 can be used to render it in a HTML template.
78 """
79 """
79 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
80 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
80
81
81 def __init__(self, diff, differ='diff', format='udiff'):
82 def __init__(self, diff, differ='diff', format='udiff'):
82 """
83 """
83 :param diff: a text in diff format or generator
84 :param diff: a text in diff format or generator
84 :param format: format of diff passed, `udiff` or `gitdiff`
85 :param format: format of diff passed, `udiff` or `gitdiff`
85 """
86 """
86 if isinstance(diff, basestring):
87 if isinstance(diff, basestring):
87 diff = [diff]
88 diff = [diff]
88
89
89 self.__udiff = diff
90 self.__udiff = diff
90 self.__format = format
91 self.__format = format
91 self.adds = 0
92 self.adds = 0
92 self.removes = 0
93 self.removes = 0
93
94
94 if isinstance(self.__udiff, basestring):
95 if isinstance(self.__udiff, basestring):
95 self.lines = iter(self.__udiff.splitlines(1))
96 self.lines = iter(self.__udiff.splitlines(1))
96
97
97 elif self.__format == 'gitdiff':
98 elif self.__format == 'gitdiff':
98 udiff_copy = self.copy_iterator()
99 udiff_copy = self.copy_iterator()
99 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
100 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
100 else:
101 else:
101 udiff_copy = self.copy_iterator()
102 udiff_copy = self.copy_iterator()
102 self.lines = imap(self.escaper, udiff_copy)
103 self.lines = imap(self.escaper, udiff_copy)
103
104
104 # Select a differ.
105 # Select a differ.
105 if differ == 'difflib':
106 if differ == 'difflib':
106 self.differ = self._highlight_line_difflib
107 self.differ = self._highlight_line_difflib
107 else:
108 else:
108 self.differ = self._highlight_line_udiff
109 self.differ = self._highlight_line_udiff
109
110
110 def escaper(self, string):
111 def escaper(self, string):
111 return string.replace('<', '&lt;').replace('>', '&gt;')
112 return string.replace('<', '&lt;').replace('>', '&gt;')
112
113
113 def copy_iterator(self):
114 def copy_iterator(self):
114 """
115 """
115 make a fresh copy of generator, we should not iterate thru
116 make a fresh copy of generator, we should not iterate thru
116 an original as it's needed for repeating operations on
117 an original as it's needed for repeating operations on
117 this instance of DiffProcessor
118 this instance of DiffProcessor
118 """
119 """
119 self.__udiff, iterator_copy = tee(self.__udiff)
120 self.__udiff, iterator_copy = tee(self.__udiff)
120 return iterator_copy
121 return iterator_copy
121
122
122 def _extract_rev(self, line1, line2):
123 def _extract_rev(self, line1, line2):
123 """
124 """
124 Extract the filename and revision hint from a line.
125 Extract the filename and revision hint from a line.
125 """
126 """
126
127
127 try:
128 try:
128 if line1.startswith('--- ') and line2.startswith('+++ '):
129 if line1.startswith('--- ') and line2.startswith('+++ '):
129 l1 = line1[4:].split(None, 1)
130 l1 = line1[4:].split(None, 1)
130 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
131 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
131 old_rev = l1[1] if len(l1) == 2 else 'old'
132 old_rev = l1[1] if len(l1) == 2 else 'old'
132
133
133 l2 = line2[4:].split(None, 1)
134 l2 = line2[4:].split(None, 1)
134 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
135 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
135 new_rev = l2[1] if len(l2) == 2 else 'new'
136 new_rev = l2[1] if len(l2) == 2 else 'new'
136
137
137 filename = old_filename if (old_filename !=
138 filename = old_filename if (old_filename !=
138 'dev/null') else new_filename
139 'dev/null') else new_filename
139
140
140 return filename, new_rev, old_rev
141 return filename, new_rev, old_rev
141 except (ValueError, IndexError):
142 except (ValueError, IndexError):
142 pass
143 pass
143
144
144 return None, None, None
145 return None, None, None
145
146
146 def _parse_gitdiff(self, diffiterator):
147 def _parse_gitdiff(self, diffiterator):
147 def line_decoder(l):
148 def line_decoder(l):
148 if l.startswith('+') and not l.startswith('+++'):
149 if l.startswith('+') and not l.startswith('+++'):
149 self.adds += 1
150 self.adds += 1
150 elif l.startswith('-') and not l.startswith('---'):
151 elif l.startswith('-') and not l.startswith('---'):
151 self.removes += 1
152 self.removes += 1
152 return l.decode('utf8', 'replace')
153 return safe_unicode(l)
153
154
154 output = list(diffiterator)
155 output = list(diffiterator)
155 size = len(output)
156 size = len(output)
156
157
157 if size == 2:
158 if size == 2:
158 l = []
159 l = []
159 l.extend([output[0]])
160 l.extend([output[0]])
160 l.extend(output[1].splitlines(1))
161 l.extend(output[1].splitlines(1))
161 return map(line_decoder, l)
162 return map(line_decoder, l)
162 elif size == 1:
163 elif size == 1:
163 return map(line_decoder, output[0].splitlines(1))
164 return map(line_decoder, output[0].splitlines(1))
164 elif size == 0:
165 elif size == 0:
165 return []
166 return []
166
167
167 raise Exception('wrong size of diff %s' % size)
168 raise Exception('wrong size of diff %s' % size)
168
169
169 def _highlight_line_difflib(self, line, next):
170 def _highlight_line_difflib(self, line, next):
170 """
171 """
171 Highlight inline changes in both lines.
172 Highlight inline changes in both lines.
172 """
173 """
173
174
174 if line['action'] == 'del':
175 if line['action'] == 'del':
175 old, new = line, next
176 old, new = line, next
176 else:
177 else:
177 old, new = next, line
178 old, new = next, line
178
179
179 oldwords = re.split(r'(\W)', old['line'])
180 oldwords = re.split(r'(\W)', old['line'])
180 newwords = re.split(r'(\W)', new['line'])
181 newwords = re.split(r'(\W)', new['line'])
181
182
182 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
183 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
183
184
184 oldfragments, newfragments = [], []
185 oldfragments, newfragments = [], []
185 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
186 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
186 oldfrag = ''.join(oldwords[i1:i2])
187 oldfrag = ''.join(oldwords[i1:i2])
187 newfrag = ''.join(newwords[j1:j2])
188 newfrag = ''.join(newwords[j1:j2])
188 if tag != 'equal':
189 if tag != 'equal':
189 if oldfrag:
190 if oldfrag:
190 oldfrag = '<del>%s</del>' % oldfrag
191 oldfrag = '<del>%s</del>' % oldfrag
191 if newfrag:
192 if newfrag:
192 newfrag = '<ins>%s</ins>' % newfrag
193 newfrag = '<ins>%s</ins>' % newfrag
193 oldfragments.append(oldfrag)
194 oldfragments.append(oldfrag)
194 newfragments.append(newfrag)
195 newfragments.append(newfrag)
195
196
196 old['line'] = "".join(oldfragments)
197 old['line'] = "".join(oldfragments)
197 new['line'] = "".join(newfragments)
198 new['line'] = "".join(newfragments)
198
199
199 def _highlight_line_udiff(self, line, next):
200 def _highlight_line_udiff(self, line, next):
200 """
201 """
201 Highlight inline changes in both lines.
202 Highlight inline changes in both lines.
202 """
203 """
203 start = 0
204 start = 0
204 limit = min(len(line['line']), len(next['line']))
205 limit = min(len(line['line']), len(next['line']))
205 while start < limit and line['line'][start] == next['line'][start]:
206 while start < limit and line['line'][start] == next['line'][start]:
206 start += 1
207 start += 1
207 end = -1
208 end = -1
208 limit -= start
209 limit -= start
209 while -end <= limit and line['line'][end] == next['line'][end]:
210 while -end <= limit and line['line'][end] == next['line'][end]:
210 end -= 1
211 end -= 1
211 end += 1
212 end += 1
212 if start or end:
213 if start or end:
213 def do(l):
214 def do(l):
214 last = end + len(l['line'])
215 last = end + len(l['line'])
215 if l['action'] == 'add':
216 if l['action'] == 'add':
216 tag = 'ins'
217 tag = 'ins'
217 else:
218 else:
218 tag = 'del'
219 tag = 'del'
219 l['line'] = '%s<%s>%s</%s>%s' % (
220 l['line'] = '%s<%s>%s</%s>%s' % (
220 l['line'][:start],
221 l['line'][:start],
221 tag,
222 tag,
222 l['line'][start:last],
223 l['line'][start:last],
223 tag,
224 tag,
224 l['line'][last:]
225 l['line'][last:]
225 )
226 )
226 do(line)
227 do(line)
227 do(next)
228 do(next)
228
229
229 def _parse_udiff(self):
230 def _parse_udiff(self):
230 """
231 """
231 Parse the diff an return data for the template.
232 Parse the diff an return data for the template.
232 """
233 """
233 lineiter = self.lines
234 lineiter = self.lines
234 files = []
235 files = []
235 try:
236 try:
236 line = lineiter.next()
237 line = lineiter.next()
237 # skip first context
238 # skip first context
238 skipfirst = True
239 skipfirst = True
239 while 1:
240 while 1:
240 # continue until we found the old file
241 # continue until we found the old file
241 if not line.startswith('--- '):
242 if not line.startswith('--- '):
242 line = lineiter.next()
243 line = lineiter.next()
243 continue
244 continue
244
245
245 chunks = []
246 chunks = []
246 filename, old_rev, new_rev = \
247 filename, old_rev, new_rev = \
247 self._extract_rev(line, lineiter.next())
248 self._extract_rev(line, lineiter.next())
248 files.append({
249 files.append({
249 'filename': filename,
250 'filename': filename,
250 'old_revision': old_rev,
251 'old_revision': old_rev,
251 'new_revision': new_rev,
252 'new_revision': new_rev,
252 'chunks': chunks
253 'chunks': chunks
253 })
254 })
254
255
255 line = lineiter.next()
256 line = lineiter.next()
256 while line:
257 while line:
257 match = self._chunk_re.match(line)
258 match = self._chunk_re.match(line)
258 if not match:
259 if not match:
259 break
260 break
260
261
261 lines = []
262 lines = []
262 chunks.append(lines)
263 chunks.append(lines)
263
264
264 old_line, old_end, new_line, new_end = \
265 old_line, old_end, new_line, new_end = \
265 [int(x or 1) for x in match.groups()[:-1]]
266 [int(x or 1) for x in match.groups()[:-1]]
266 old_line -= 1
267 old_line -= 1
267 new_line -= 1
268 new_line -= 1
268 context = len(match.groups()) == 5
269 context = len(match.groups()) == 5
269 old_end += old_line
270 old_end += old_line
270 new_end += new_line
271 new_end += new_line
271
272
272 if context:
273 if context:
273 if not skipfirst:
274 if not skipfirst:
274 lines.append({
275 lines.append({
275 'old_lineno': '...',
276 'old_lineno': '...',
276 'new_lineno': '...',
277 'new_lineno': '...',
277 'action': 'context',
278 'action': 'context',
278 'line': line,
279 'line': line,
279 })
280 })
280 else:
281 else:
281 skipfirst = False
282 skipfirst = False
282
283
283 line = lineiter.next()
284 line = lineiter.next()
284 while old_line < old_end or new_line < new_end:
285 while old_line < old_end or new_line < new_end:
285 if line:
286 if line:
286 command, line = line[0], line[1:]
287 command, line = line[0], line[1:]
287 else:
288 else:
288 command = ' '
289 command = ' '
289 affects_old = affects_new = False
290 affects_old = affects_new = False
290
291
291 # ignore those if we don't expect them
292 # ignore those if we don't expect them
292 if command in '#@':
293 if command in '#@':
293 continue
294 continue
294 elif command == '+':
295 elif command == '+':
295 affects_new = True
296 affects_new = True
296 action = 'add'
297 action = 'add'
297 elif command == '-':
298 elif command == '-':
298 affects_old = True
299 affects_old = True
299 action = 'del'
300 action = 'del'
300 else:
301 else:
301 affects_old = affects_new = True
302 affects_old = affects_new = True
302 action = 'unmod'
303 action = 'unmod'
303
304
304 old_line += affects_old
305 old_line += affects_old
305 new_line += affects_new
306 new_line += affects_new
306 lines.append({
307 lines.append({
307 'old_lineno': affects_old and old_line or '',
308 'old_lineno': affects_old and old_line or '',
308 'new_lineno': affects_new and new_line or '',
309 'new_lineno': affects_new and new_line or '',
309 'action': action,
310 'action': action,
310 'line': line
311 'line': line
311 })
312 })
312 line = lineiter.next()
313 line = lineiter.next()
313
314
314 except StopIteration:
315 except StopIteration:
315 pass
316 pass
316
317
317 # highlight inline changes
318 # highlight inline changes
318 for file in files:
319 for file in files:
319 for chunk in chunks:
320 for chunk in chunks:
320 lineiter = iter(chunk)
321 lineiter = iter(chunk)
321 #first = True
322 #first = True
322 try:
323 try:
323 while 1:
324 while 1:
324 line = lineiter.next()
325 line = lineiter.next()
325 if line['action'] != 'unmod':
326 if line['action'] != 'unmod':
326 nextline = lineiter.next()
327 nextline = lineiter.next()
327 if nextline['action'] == 'unmod' or \
328 if nextline['action'] == 'unmod' or \
328 nextline['action'] == line['action']:
329 nextline['action'] == line['action']:
329 continue
330 continue
330 self.differ(line, nextline)
331 self.differ(line, nextline)
331 except StopIteration:
332 except StopIteration:
332 pass
333 pass
333
334
334 return files
335 return files
335
336
336 def prepare(self):
337 def prepare(self):
337 """
338 """
338 Prepare the passed udiff for HTML rendering. It'l return a list
339 Prepare the passed udiff for HTML rendering. It'l return a list
339 of dicts
340 of dicts
340 """
341 """
341 return self._parse_udiff()
342 return self._parse_udiff()
342
343
343 def _safe_id(self, idstring):
344 def _safe_id(self, idstring):
344 """Make a string safe for including in an id attribute.
345 """Make a string safe for including in an id attribute.
345
346
346 The HTML spec says that id attributes 'must begin with
347 The HTML spec says that id attributes 'must begin with
347 a letter ([A-Za-z]) and may be followed by any number
348 a letter ([A-Za-z]) and may be followed by any number
348 of letters, digits ([0-9]), hyphens ("-"), underscores
349 of letters, digits ([0-9]), hyphens ("-"), underscores
349 ("_"), colons (":"), and periods (".")'. These regexps
350 ("_"), colons (":"), and periods (".")'. These regexps
350 are slightly over-zealous, in that they remove colons
351 are slightly over-zealous, in that they remove colons
351 and periods unnecessarily.
352 and periods unnecessarily.
352
353
353 Whitespace is transformed into underscores, and then
354 Whitespace is transformed into underscores, and then
354 anything which is not a hyphen or a character that
355 anything which is not a hyphen or a character that
355 matches \w (alphanumerics and underscore) is removed.
356 matches \w (alphanumerics and underscore) is removed.
356
357
357 """
358 """
358 # Transform all whitespace to underscore
359 # Transform all whitespace to underscore
359 idstring = re.sub(r'\s', "_", '%s' % idstring)
360 idstring = re.sub(r'\s', "_", '%s' % idstring)
360 # Remove everything that is not a hyphen or a member of \w
361 # Remove everything that is not a hyphen or a member of \w
361 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
362 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
362 return idstring
363 return idstring
363
364
364 def raw_diff(self):
365 def raw_diff(self):
365 """
366 """
366 Returns raw string as udiff
367 Returns raw string as udiff
367 """
368 """
368 udiff_copy = self.copy_iterator()
369 udiff_copy = self.copy_iterator()
369 if self.__format == 'gitdiff':
370 if self.__format == 'gitdiff':
370 udiff_copy = self._parse_gitdiff(udiff_copy)
371 udiff_copy = self._parse_gitdiff(udiff_copy)
371 return u''.join(udiff_copy)
372 return u''.join(udiff_copy)
372
373
373 def as_html(self, table_class='code-difftable', line_class='line',
374 def as_html(self, table_class='code-difftable', line_class='line',
374 new_lineno_class='lineno old', old_lineno_class='lineno new',
375 new_lineno_class='lineno old', old_lineno_class='lineno new',
375 code_class='code'):
376 code_class='code'):
376 """
377 """
377 Return udiff as html table with customized css classes
378 Return udiff as html table with customized css classes
378 """
379 """
379 def _link_to_if(condition, label, url):
380 def _link_to_if(condition, label, url):
380 """
381 """
381 Generates a link if condition is meet or just the label if not.
382 Generates a link if condition is meet or just the label if not.
382 """
383 """
383
384
384 if condition:
385 if condition:
385 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
386 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
386 'label': label}
387 'label': label}
387 else:
388 else:
388 return label
389 return label
389 diff_lines = self.prepare()
390 diff_lines = self.prepare()
390 _html_empty = True
391 _html_empty = True
391 _html = []
392 _html = []
392 _html.append('''<table class="%(table_class)s">\n''' \
393 _html.append('''<table class="%(table_class)s">\n''' \
393 % {'table_class': table_class})
394 % {'table_class': table_class})
394 for diff in diff_lines:
395 for diff in diff_lines:
395 for line in diff['chunks']:
396 for line in diff['chunks']:
396 _html_empty = False
397 _html_empty = False
397 for change in line:
398 for change in line:
398 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
399 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
399 % {'line_class': line_class,
400 % {'line_class': line_class,
400 'action': change['action']})
401 'action': change['action']})
401 anchor_old_id = ''
402 anchor_old_id = ''
402 anchor_new_id = ''
403 anchor_new_id = ''
403 anchor_old = "%(filename)s_o%(oldline_no)s" % \
404 anchor_old = "%(filename)s_o%(oldline_no)s" % \
404 {'filename': self._safe_id(diff['filename']),
405 {'filename': self._safe_id(diff['filename']),
405 'oldline_no': change['old_lineno']}
406 'oldline_no': change['old_lineno']}
406 anchor_new = "%(filename)s_n%(oldline_no)s" % \
407 anchor_new = "%(filename)s_n%(oldline_no)s" % \
407 {'filename': self._safe_id(diff['filename']),
408 {'filename': self._safe_id(diff['filename']),
408 'oldline_no': change['new_lineno']}
409 'oldline_no': change['new_lineno']}
409 cond_old = change['old_lineno'] != '...' and \
410 cond_old = change['old_lineno'] != '...' and \
410 change['old_lineno']
411 change['old_lineno']
411 cond_new = change['new_lineno'] != '...' and \
412 cond_new = change['new_lineno'] != '...' and \
412 change['new_lineno']
413 change['new_lineno']
413 if cond_old:
414 if cond_old:
414 anchor_old_id = 'id="%s"' % anchor_old
415 anchor_old_id = 'id="%s"' % anchor_old
415 if cond_new:
416 if cond_new:
416 anchor_new_id = 'id="%s"' % anchor_new
417 anchor_new_id = 'id="%s"' % anchor_new
417 ###########################################################
418 ###########################################################
418 # OLD LINE NUMBER
419 # OLD LINE NUMBER
419 ###########################################################
420 ###########################################################
420 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
421 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
421 % {'a_id': anchor_old_id,
422 % {'a_id': anchor_old_id,
422 'old_lineno_cls': old_lineno_class})
423 'old_lineno_cls': old_lineno_class})
423
424
424 _html.append('''<pre>%(link)s</pre>''' \
425 _html.append('''<pre>%(link)s</pre>''' \
425 % {'link':
426 % {'link':
426 _link_to_if(cond_old, change['old_lineno'], '#%s' \
427 _link_to_if(cond_old, change['old_lineno'], '#%s' \
427 % anchor_old)})
428 % anchor_old)})
428 _html.append('''</td>\n''')
429 _html.append('''</td>\n''')
429 ###########################################################
430 ###########################################################
430 # NEW LINE NUMBER
431 # NEW LINE NUMBER
431 ###########################################################
432 ###########################################################
432
433
433 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
434 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
434 % {'a_id': anchor_new_id,
435 % {'a_id': anchor_new_id,
435 'new_lineno_cls': new_lineno_class})
436 'new_lineno_cls': new_lineno_class})
436
437
437 _html.append('''<pre>%(link)s</pre>''' \
438 _html.append('''<pre>%(link)s</pre>''' \
438 % {'link':
439 % {'link':
439 _link_to_if(cond_new, change['new_lineno'], '#%s' \
440 _link_to_if(cond_new, change['new_lineno'], '#%s' \
440 % anchor_new)})
441 % anchor_new)})
441 _html.append('''</td>\n''')
442 _html.append('''</td>\n''')
442 ###########################################################
443 ###########################################################
443 # CODE
444 # CODE
444 ###########################################################
445 ###########################################################
445 _html.append('''\t<td class="%(code_class)s">''' \
446 _html.append('''\t<td class="%(code_class)s">''' \
446 % {'code_class': code_class})
447 % {'code_class': code_class})
447 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
448 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
448 % {'code': change['line']})
449 % {'code': change['line']})
449 _html.append('''\t</td>''')
450 _html.append('''\t</td>''')
450 _html.append('''\n</tr>\n''')
451 _html.append('''\n</tr>\n''')
451 _html.append('''</table>''')
452 _html.append('''</table>''')
452 if _html_empty:
453 if _html_empty:
453 return None
454 return None
454 return ''.join(_html)
455 return ''.join(_html)
455
456
456 def stat(self):
457 def stat(self):
457 """
458 """
458 Returns tuple of adde,and removed lines for this instance
459 Returns tuple of adde,and removed lines for this instance
459 """
460 """
460 return self.adds, self.removes
461 return self.adds, self.removes
General Comments 0
You need to be logged in to leave comments. Login now