##// END OF EJS Templates
fix for latest vcs
marcink -
r1883:69d3c445 beta
parent child Browse files
Show More
@@ -1,515 +1,515 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7 7
8 8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import difflib
30 30 import markupsafe
31 31 from itertools import tee, imap
32 32
33 33 from pylons.i18n.translation import _
34 34
35 35 from vcs.exceptions import VCSError
36 36 from vcs.nodes import FileNode
37 37
38 38 from rhodecode.lib.utils import EmptyChangeset
39 39
40 40
41 41 def wrap_to_table(str_):
42 42 return '''<table class="code-difftable">
43 43 <tr class="line no-comment">
44 44 <td class="lineno new"></td>
45 45 <td class="code no-comment"><pre>%s</pre></td>
46 46 </tr>
47 47 </table>''' % str_
48 48
49 49
50 50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 51 ignore_whitespace=True, line_context=3,
52 52 enable_comments=False):
53 53 """
54 54 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 55 proper message
56 56 """
57 57
58 58 if filenode_old is None:
59 59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60 60
61 61 if filenode_old.is_binary or filenode_new.is_binary:
62 62 diff = wrap_to_table(_('binary file'))
63 63 stats = (0, 0)
64 64 size = 0
65 65
66 66 elif cut_off_limit != -1 and (cut_off_limit is None or
67 67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68 68
69 69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 70 ignore_whitespace=ignore_whitespace,
71 71 context=line_context)
72 72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73 73
74 74 diff = diff_processor.as_html(enable_comments=enable_comments)
75 75 stats = diff_processor.stat()
76 76 size = len(diff or '')
77 77 else:
78 78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
79 79 'diff menu to display this diff'))
80 80 stats = (0, 0)
81 81 size = 0
82 82
83 83 if not diff:
84 84 diff = wrap_to_table(_('No changes detected'))
85 85
86 86 cs1 = filenode_old.last_changeset.raw_id
87 87 cs2 = filenode_new.last_changeset.raw_id
88 88
89 89 return size, cs1, cs2, diff, stats
90 90
91 91
92 92 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
93 93 """
94 94 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
95 95
96 96 :param ignore_whitespace: ignore whitespaces in diff
97 97 """
98 98
99 99 for filenode in (filenode_old, filenode_new):
100 100 if not isinstance(filenode, FileNode):
101 101 raise VCSError("Given object should be FileNode object, not %s"
102 102 % filenode.__class__)
103 103
104 104 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
105 105 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
106 106
107 107 repo = filenode_new.changeset.repository
108 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
108 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
109 109 ignore_whitespace, context)
110 110
111 111 return vcs_gitdiff
112 112
113 113
114 114 class DiffProcessor(object):
115 115 """
116 116 Give it a unified diff and it returns a list of the files that were
117 117 mentioned in the diff together with a dict of meta information that
118 118 can be used to render it in a HTML template.
119 119 """
120 120 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
121 121
122 122 def __init__(self, diff, differ='diff', format='udiff'):
123 123 """
124 124 :param diff: a text in diff format or generator
125 125 :param format: format of diff passed, `udiff` or `gitdiff`
126 126 """
127 127 if isinstance(diff, basestring):
128 128 diff = [diff]
129 129
130 130 self.__udiff = diff
131 131 self.__format = format
132 132 self.adds = 0
133 133 self.removes = 0
134 134
135 135 if isinstance(self.__udiff, basestring):
136 136 self.lines = iter(self.__udiff.splitlines(1))
137 137
138 138 elif self.__format == 'gitdiff':
139 139 udiff_copy = self.copy_iterator()
140 140 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
141 141 else:
142 142 udiff_copy = self.copy_iterator()
143 143 self.lines = imap(self.escaper, udiff_copy)
144 144
145 145 # Select a differ.
146 146 if differ == 'difflib':
147 147 self.differ = self._highlight_line_difflib
148 148 else:
149 149 self.differ = self._highlight_line_udiff
150 150
151 151 def escaper(self, string):
152 152 return markupsafe.escape(string)
153 153
154 154 def copy_iterator(self):
155 155 """
156 156 make a fresh copy of generator, we should not iterate thru
157 157 an original as it's needed for repeating operations on
158 158 this instance of DiffProcessor
159 159 """
160 160 self.__udiff, iterator_copy = tee(self.__udiff)
161 161 return iterator_copy
162 162
163 163 def _extract_rev(self, line1, line2):
164 164 """
165 165 Extract the filename and revision hint from a line.
166 166 """
167 167
168 168 try:
169 169 if line1.startswith('--- ') and line2.startswith('+++ '):
170 170 l1 = line1[4:].split(None, 1)
171 171 old_filename = (l1[0].replace('a/', '', 1)
172 172 if len(l1) >= 1 else None)
173 173 old_rev = l1[1] if len(l1) == 2 else 'old'
174 174
175 175 l2 = line2[4:].split(None, 1)
176 176 new_filename = (l2[0].replace('b/', '', 1)
177 177 if len(l1) >= 1 else None)
178 178 new_rev = l2[1] if len(l2) == 2 else 'new'
179 179
180 180 filename = (old_filename
181 181 if old_filename != '/dev/null' else new_filename)
182 182
183 183 return filename, new_rev, old_rev
184 184 except (ValueError, IndexError):
185 185 pass
186 186
187 187 return None, None, None
188 188
189 189 def _parse_gitdiff(self, diffiterator):
190 190 def line_decoder(l):
191 191 if l.startswith('+') and not l.startswith('+++'):
192 192 self.adds += 1
193 193 elif l.startswith('-') and not l.startswith('---'):
194 194 self.removes += 1
195 195 return l.decode('utf8', 'replace')
196 196
197 197 output = list(diffiterator)
198 198 size = len(output)
199 199
200 200 if size == 2:
201 201 l = []
202 202 l.extend([output[0]])
203 203 l.extend(output[1].splitlines(1))
204 204 return map(line_decoder, l)
205 205 elif size == 1:
206 206 return map(line_decoder, output[0].splitlines(1))
207 207 elif size == 0:
208 208 return []
209 209
210 210 raise Exception('wrong size of diff %s' % size)
211 211
212 212 def _highlight_line_difflib(self, line, next_):
213 213 """
214 214 Highlight inline changes in both lines.
215 215 """
216 216
217 217 if line['action'] == 'del':
218 218 old, new = line, next_
219 219 else:
220 220 old, new = next_, line
221 221
222 222 oldwords = re.split(r'(\W)', old['line'])
223 223 newwords = re.split(r'(\W)', new['line'])
224 224
225 225 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
226 226
227 227 oldfragments, newfragments = [], []
228 228 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
229 229 oldfrag = ''.join(oldwords[i1:i2])
230 230 newfrag = ''.join(newwords[j1:j2])
231 231 if tag != 'equal':
232 232 if oldfrag:
233 233 oldfrag = '<del>%s</del>' % oldfrag
234 234 if newfrag:
235 235 newfrag = '<ins>%s</ins>' % newfrag
236 236 oldfragments.append(oldfrag)
237 237 newfragments.append(newfrag)
238 238
239 239 old['line'] = "".join(oldfragments)
240 240 new['line'] = "".join(newfragments)
241 241
242 242 def _highlight_line_udiff(self, line, next_):
243 243 """
244 244 Highlight inline changes in both lines.
245 245 """
246 246 start = 0
247 247 limit = min(len(line['line']), len(next_['line']))
248 248 while start < limit and line['line'][start] == next_['line'][start]:
249 249 start += 1
250 250 end = -1
251 251 limit -= start
252 252 while -end <= limit and line['line'][end] == next_['line'][end]:
253 253 end -= 1
254 254 end += 1
255 255 if start or end:
256 256 def do(l):
257 257 last = end + len(l['line'])
258 258 if l['action'] == 'add':
259 259 tag = 'ins'
260 260 else:
261 261 tag = 'del'
262 262 l['line'] = '%s<%s>%s</%s>%s' % (
263 263 l['line'][:start],
264 264 tag,
265 265 l['line'][start:last],
266 266 tag,
267 267 l['line'][last:]
268 268 )
269 269 do(line)
270 270 do(next_)
271 271
272 272 def _parse_udiff(self):
273 273 """
274 274 Parse the diff an return data for the template.
275 275 """
276 276 lineiter = self.lines
277 277 files = []
278 278 try:
279 279 line = lineiter.next()
280 280 # skip first context
281 281 skipfirst = True
282 282 while 1:
283 283 # continue until we found the old file
284 284 if not line.startswith('--- '):
285 285 line = lineiter.next()
286 286 continue
287 287
288 288 chunks = []
289 289 filename, old_rev, new_rev = \
290 290 self._extract_rev(line, lineiter.next())
291 291 files.append({
292 292 'filename': filename,
293 293 'old_revision': old_rev,
294 294 'new_revision': new_rev,
295 295 'chunks': chunks
296 296 })
297 297
298 298 line = lineiter.next()
299 299 while line:
300 300 match = self._chunk_re.match(line)
301 301 if not match:
302 302 break
303 303
304 304 lines = []
305 305 chunks.append(lines)
306 306
307 307 old_line, old_end, new_line, new_end = \
308 308 [int(x or 1) for x in match.groups()[:-1]]
309 309 old_line -= 1
310 310 new_line -= 1
311 311 context = len(match.groups()) == 5
312 312 old_end += old_line
313 313 new_end += new_line
314 314
315 315 if context:
316 316 if not skipfirst:
317 317 lines.append({
318 318 'old_lineno': '...',
319 319 'new_lineno': '...',
320 320 'action': 'context',
321 321 'line': line,
322 322 })
323 323 else:
324 324 skipfirst = False
325 325
326 326 line = lineiter.next()
327 327 while old_line < old_end or new_line < new_end:
328 328 if line:
329 329 command, line = line[0], line[1:]
330 330 else:
331 331 command = ' '
332 332 affects_old = affects_new = False
333 333
334 334 # ignore those if we don't expect them
335 335 if command in '#@':
336 336 continue
337 337 elif command == '+':
338 338 affects_new = True
339 339 action = 'add'
340 340 elif command == '-':
341 341 affects_old = True
342 342 action = 'del'
343 343 else:
344 344 affects_old = affects_new = True
345 345 action = 'unmod'
346 346
347 347 old_line += affects_old
348 348 new_line += affects_new
349 349 lines.append({
350 350 'old_lineno': affects_old and old_line or '',
351 351 'new_lineno': affects_new and new_line or '',
352 352 'action': action,
353 353 'line': line
354 354 })
355 355 line = lineiter.next()
356 356
357 357 except StopIteration:
358 358 pass
359 359
360 360 # highlight inline changes
361 361 for _ in files:
362 362 for chunk in chunks:
363 363 lineiter = iter(chunk)
364 364 #first = True
365 365 try:
366 366 while 1:
367 367 line = lineiter.next()
368 368 if line['action'] != 'unmod':
369 369 nextline = lineiter.next()
370 370 if nextline['action'] == 'unmod' or \
371 371 nextline['action'] == line['action']:
372 372 continue
373 373 self.differ(line, nextline)
374 374 except StopIteration:
375 375 pass
376 376
377 377 return files
378 378
379 379 def prepare(self):
380 380 """
381 381 Prepare the passed udiff for HTML rendering. It'l return a list
382 382 of dicts
383 383 """
384 384 return self._parse_udiff()
385 385
386 386 def _safe_id(self, idstring):
387 387 """Make a string safe for including in an id attribute.
388 388
389 389 The HTML spec says that id attributes 'must begin with
390 390 a letter ([A-Za-z]) and may be followed by any number
391 391 of letters, digits ([0-9]), hyphens ("-"), underscores
392 392 ("_"), colons (":"), and periods (".")'. These regexps
393 393 are slightly over-zealous, in that they remove colons
394 394 and periods unnecessarily.
395 395
396 396 Whitespace is transformed into underscores, and then
397 397 anything which is not a hyphen or a character that
398 398 matches \w (alphanumerics and underscore) is removed.
399 399
400 400 """
401 401 # Transform all whitespace to underscore
402 402 idstring = re.sub(r'\s', "_", '%s' % idstring)
403 403 # Remove everything that is not a hyphen or a member of \w
404 404 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
405 405 return idstring
406 406
407 407 def raw_diff(self):
408 408 """
409 409 Returns raw string as udiff
410 410 """
411 411 udiff_copy = self.copy_iterator()
412 412 if self.__format == 'gitdiff':
413 413 udiff_copy = self._parse_gitdiff(udiff_copy)
414 414 return u''.join(udiff_copy)
415 415
416 416 def as_html(self, table_class='code-difftable', line_class='line',
417 417 new_lineno_class='lineno old', old_lineno_class='lineno new',
418 418 code_class='code', enable_comments=False):
419 419 """
420 420 Return udiff as html table with customized css classes
421 421 """
422 422 def _link_to_if(condition, label, url):
423 423 """
424 424 Generates a link if condition is meet or just the label if not.
425 425 """
426 426
427 427 if condition:
428 428 return '''<a href="%(url)s">%(label)s</a>''' % {
429 429 'url': url,
430 430 'label': label
431 431 }
432 432 else:
433 433 return label
434 434 diff_lines = self.prepare()
435 435 _html_empty = True
436 436 _html = []
437 437 _html.append('''<table class="%(table_class)s">\n''' % {
438 438 'table_class': table_class
439 439 })
440 440 for diff in diff_lines:
441 441 for line in diff['chunks']:
442 442 _html_empty = False
443 443 for change in line:
444 444 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
445 445 'lc': line_class,
446 446 'action': change['action']
447 447 })
448 448 anchor_old_id = ''
449 449 anchor_new_id = ''
450 450 anchor_old = "%(filename)s_o%(oldline_no)s" % {
451 451 'filename': self._safe_id(diff['filename']),
452 452 'oldline_no': change['old_lineno']
453 453 }
454 454 anchor_new = "%(filename)s_n%(oldline_no)s" % {
455 455 'filename': self._safe_id(diff['filename']),
456 456 'oldline_no': change['new_lineno']
457 457 }
458 458 cond_old = (change['old_lineno'] != '...' and
459 459 change['old_lineno'])
460 460 cond_new = (change['new_lineno'] != '...' and
461 461 change['new_lineno'])
462 462 if cond_old:
463 463 anchor_old_id = 'id="%s"' % anchor_old
464 464 if cond_new:
465 465 anchor_new_id = 'id="%s"' % anchor_new
466 466 ###########################################################
467 467 # OLD LINE NUMBER
468 468 ###########################################################
469 469 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
470 470 'a_id': anchor_old_id,
471 471 'olc': old_lineno_class
472 472 })
473 473
474 474 _html.append('''%(link)s''' % {
475 475 'link': _link_to_if(True, change['old_lineno'],
476 476 '#%s' % anchor_old)
477 477 })
478 478 _html.append('''</td>\n''')
479 479 ###########################################################
480 480 # NEW LINE NUMBER
481 481 ###########################################################
482 482
483 483 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
484 484 'a_id': anchor_new_id,
485 485 'nlc': new_lineno_class
486 486 })
487 487
488 488 _html.append('''%(link)s''' % {
489 489 'link': _link_to_if(True, change['new_lineno'],
490 490 '#%s' % anchor_new)
491 491 })
492 492 _html.append('''</td>\n''')
493 493 ###########################################################
494 494 # CODE
495 495 ###########################################################
496 496 comments = '' if enable_comments else 'no-comment'
497 497 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
498 498 'cc': code_class,
499 499 'inc': comments
500 500 })
501 501 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
502 502 'code': change['line']
503 503 })
504 504 _html.append('''\t</td>''')
505 505 _html.append('''\n</tr>\n''')
506 506 _html.append('''</table>''')
507 507 if _html_empty:
508 508 return None
509 509 return ''.join(_html)
510 510
511 511 def stat(self):
512 512 """
513 513 Returns tuple of added, and removed lines for this instance
514 514 """
515 515 return self.adds, self.removes
General Comments 0
You need to be logged in to leave comments. Login now