##// END OF EJS Templates
adapt codes to latest vcs
marcink -
r1894:0cbc3fb5 beta
parent child Browse files
Show More
@@ -1,515 +1,517 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 # make sure we pass in default context
99 context = context or 3
98 100
99 101 for filenode in (filenode_old, filenode_new):
100 102 if not isinstance(filenode, FileNode):
101 103 raise VCSError("Given object should be FileNode object, not %s"
102 104 % filenode.__class__)
103 105
104 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
105 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
106 repo = filenode_new.changeset.repository
107 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
108 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
106 109
107 repo = filenode_new.changeset.repository
108 110 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
109 111 ignore_whitespace, context)
110 112
111 113 return vcs_gitdiff
112 114
113 115
114 116 class DiffProcessor(object):
115 117 """
116 118 Give it a unified diff and it returns a list of the files that were
117 119 mentioned in the diff together with a dict of meta information that
118 120 can be used to render it in a HTML template.
119 121 """
120 122 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
121 123
122 124 def __init__(self, diff, differ='diff', format='udiff'):
123 125 """
124 126 :param diff: a text in diff format or generator
125 127 :param format: format of diff passed, `udiff` or `gitdiff`
126 128 """
127 129 if isinstance(diff, basestring):
128 130 diff = [diff]
129 131
130 132 self.__udiff = diff
131 133 self.__format = format
132 134 self.adds = 0
133 135 self.removes = 0
134 136
135 137 if isinstance(self.__udiff, basestring):
136 138 self.lines = iter(self.__udiff.splitlines(1))
137 139
138 140 elif self.__format == 'gitdiff':
139 141 udiff_copy = self.copy_iterator()
140 142 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
141 143 else:
142 144 udiff_copy = self.copy_iterator()
143 145 self.lines = imap(self.escaper, udiff_copy)
144 146
145 147 # Select a differ.
146 148 if differ == 'difflib':
147 149 self.differ = self._highlight_line_difflib
148 150 else:
149 151 self.differ = self._highlight_line_udiff
150 152
151 153 def escaper(self, string):
152 154 return markupsafe.escape(string)
153 155
154 156 def copy_iterator(self):
155 157 """
156 158 make a fresh copy of generator, we should not iterate thru
157 159 an original as it's needed for repeating operations on
158 160 this instance of DiffProcessor
159 161 """
160 162 self.__udiff, iterator_copy = tee(self.__udiff)
161 163 return iterator_copy
162 164
163 165 def _extract_rev(self, line1, line2):
164 166 """
165 167 Extract the filename and revision hint from a line.
166 168 """
167 169
168 170 try:
169 171 if line1.startswith('--- ') and line2.startswith('+++ '):
170 172 l1 = line1[4:].split(None, 1)
171 173 old_filename = (l1[0].replace('a/', '', 1)
172 174 if len(l1) >= 1 else None)
173 175 old_rev = l1[1] if len(l1) == 2 else 'old'
174 176
175 177 l2 = line2[4:].split(None, 1)
176 178 new_filename = (l2[0].replace('b/', '', 1)
177 179 if len(l1) >= 1 else None)
178 180 new_rev = l2[1] if len(l2) == 2 else 'new'
179 181
180 182 filename = (old_filename
181 183 if old_filename != '/dev/null' else new_filename)
182 184
183 185 return filename, new_rev, old_rev
184 186 except (ValueError, IndexError):
185 187 pass
186 188
187 189 return None, None, None
188 190
189 191 def _parse_gitdiff(self, diffiterator):
190 192 def line_decoder(l):
191 193 if l.startswith('+') and not l.startswith('+++'):
192 194 self.adds += 1
193 195 elif l.startswith('-') and not l.startswith('---'):
194 196 self.removes += 1
195 197 return l.decode('utf8', 'replace')
196 198
197 199 output = list(diffiterator)
198 200 size = len(output)
199 201
200 202 if size == 2:
201 203 l = []
202 204 l.extend([output[0]])
203 205 l.extend(output[1].splitlines(1))
204 206 return map(line_decoder, l)
205 207 elif size == 1:
206 208 return map(line_decoder, output[0].splitlines(1))
207 209 elif size == 0:
208 210 return []
209 211
210 212 raise Exception('wrong size of diff %s' % size)
211 213
212 214 def _highlight_line_difflib(self, line, next_):
213 215 """
214 216 Highlight inline changes in both lines.
215 217 """
216 218
217 219 if line['action'] == 'del':
218 220 old, new = line, next_
219 221 else:
220 222 old, new = next_, line
221 223
222 224 oldwords = re.split(r'(\W)', old['line'])
223 225 newwords = re.split(r'(\W)', new['line'])
224 226
225 227 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
226 228
227 229 oldfragments, newfragments = [], []
228 230 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
229 231 oldfrag = ''.join(oldwords[i1:i2])
230 232 newfrag = ''.join(newwords[j1:j2])
231 233 if tag != 'equal':
232 234 if oldfrag:
233 235 oldfrag = '<del>%s</del>' % oldfrag
234 236 if newfrag:
235 237 newfrag = '<ins>%s</ins>' % newfrag
236 238 oldfragments.append(oldfrag)
237 239 newfragments.append(newfrag)
238 240
239 241 old['line'] = "".join(oldfragments)
240 242 new['line'] = "".join(newfragments)
241 243
242 244 def _highlight_line_udiff(self, line, next_):
243 245 """
244 246 Highlight inline changes in both lines.
245 247 """
246 248 start = 0
247 249 limit = min(len(line['line']), len(next_['line']))
248 250 while start < limit and line['line'][start] == next_['line'][start]:
249 251 start += 1
250 252 end = -1
251 253 limit -= start
252 254 while -end <= limit and line['line'][end] == next_['line'][end]:
253 255 end -= 1
254 256 end += 1
255 257 if start or end:
256 258 def do(l):
257 259 last = end + len(l['line'])
258 260 if l['action'] == 'add':
259 261 tag = 'ins'
260 262 else:
261 263 tag = 'del'
262 264 l['line'] = '%s<%s>%s</%s>%s' % (
263 265 l['line'][:start],
264 266 tag,
265 267 l['line'][start:last],
266 268 tag,
267 269 l['line'][last:]
268 270 )
269 271 do(line)
270 272 do(next_)
271 273
272 274 def _parse_udiff(self):
273 275 """
274 276 Parse the diff an return data for the template.
275 277 """
276 278 lineiter = self.lines
277 279 files = []
278 280 try:
279 281 line = lineiter.next()
280 282 # skip first context
281 283 skipfirst = True
282 284 while 1:
283 285 # continue until we found the old file
284 286 if not line.startswith('--- '):
285 287 line = lineiter.next()
286 288 continue
287 289
288 290 chunks = []
289 291 filename, old_rev, new_rev = \
290 292 self._extract_rev(line, lineiter.next())
291 293 files.append({
292 294 'filename': filename,
293 295 'old_revision': old_rev,
294 296 'new_revision': new_rev,
295 297 'chunks': chunks
296 298 })
297 299
298 300 line = lineiter.next()
299 301 while line:
300 302 match = self._chunk_re.match(line)
301 303 if not match:
302 304 break
303 305
304 306 lines = []
305 307 chunks.append(lines)
306 308
307 309 old_line, old_end, new_line, new_end = \
308 310 [int(x or 1) for x in match.groups()[:-1]]
309 311 old_line -= 1
310 312 new_line -= 1
311 313 context = len(match.groups()) == 5
312 314 old_end += old_line
313 315 new_end += new_line
314 316
315 317 if context:
316 318 if not skipfirst:
317 319 lines.append({
318 320 'old_lineno': '...',
319 321 'new_lineno': '...',
320 322 'action': 'context',
321 323 'line': line,
322 324 })
323 325 else:
324 326 skipfirst = False
325 327
326 328 line = lineiter.next()
327 329 while old_line < old_end or new_line < new_end:
328 330 if line:
329 331 command, line = line[0], line[1:]
330 332 else:
331 333 command = ' '
332 334 affects_old = affects_new = False
333 335
334 336 # ignore those if we don't expect them
335 337 if command in '#@':
336 338 continue
337 339 elif command == '+':
338 340 affects_new = True
339 341 action = 'add'
340 342 elif command == '-':
341 343 affects_old = True
342 344 action = 'del'
343 345 else:
344 346 affects_old = affects_new = True
345 347 action = 'unmod'
346 348
347 349 old_line += affects_old
348 350 new_line += affects_new
349 351 lines.append({
350 352 'old_lineno': affects_old and old_line or '',
351 353 'new_lineno': affects_new and new_line or '',
352 354 'action': action,
353 355 'line': line
354 356 })
355 357 line = lineiter.next()
356 358
357 359 except StopIteration:
358 360 pass
359 361
360 362 # highlight inline changes
361 363 for _ in files:
362 364 for chunk in chunks:
363 365 lineiter = iter(chunk)
364 366 #first = True
365 367 try:
366 368 while 1:
367 369 line = lineiter.next()
368 370 if line['action'] != 'unmod':
369 371 nextline = lineiter.next()
370 372 if nextline['action'] == 'unmod' or \
371 373 nextline['action'] == line['action']:
372 374 continue
373 375 self.differ(line, nextline)
374 376 except StopIteration:
375 377 pass
376 378
377 379 return files
378 380
379 381 def prepare(self):
380 382 """
381 383 Prepare the passed udiff for HTML rendering. It'l return a list
382 384 of dicts
383 385 """
384 386 return self._parse_udiff()
385 387
386 388 def _safe_id(self, idstring):
387 389 """Make a string safe for including in an id attribute.
388 390
389 391 The HTML spec says that id attributes 'must begin with
390 392 a letter ([A-Za-z]) and may be followed by any number
391 393 of letters, digits ([0-9]), hyphens ("-"), underscores
392 394 ("_"), colons (":"), and periods (".")'. These regexps
393 395 are slightly over-zealous, in that they remove colons
394 396 and periods unnecessarily.
395 397
396 398 Whitespace is transformed into underscores, and then
397 399 anything which is not a hyphen or a character that
398 400 matches \w (alphanumerics and underscore) is removed.
399 401
400 402 """
401 403 # Transform all whitespace to underscore
402 404 idstring = re.sub(r'\s', "_", '%s' % idstring)
403 405 # Remove everything that is not a hyphen or a member of \w
404 406 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
405 407 return idstring
406 408
407 409 def raw_diff(self):
408 410 """
409 411 Returns raw string as udiff
410 412 """
411 413 udiff_copy = self.copy_iterator()
412 414 if self.__format == 'gitdiff':
413 415 udiff_copy = self._parse_gitdiff(udiff_copy)
414 416 return u''.join(udiff_copy)
415 417
416 418 def as_html(self, table_class='code-difftable', line_class='line',
417 419 new_lineno_class='lineno old', old_lineno_class='lineno new',
418 420 code_class='code', enable_comments=False):
419 421 """
420 422 Return udiff as html table with customized css classes
421 423 """
422 424 def _link_to_if(condition, label, url):
423 425 """
424 426 Generates a link if condition is meet or just the label if not.
425 427 """
426 428
427 429 if condition:
428 430 return '''<a href="%(url)s">%(label)s</a>''' % {
429 431 'url': url,
430 432 'label': label
431 433 }
432 434 else:
433 435 return label
434 436 diff_lines = self.prepare()
435 437 _html_empty = True
436 438 _html = []
437 439 _html.append('''<table class="%(table_class)s">\n''' % {
438 440 'table_class': table_class
439 441 })
440 442 for diff in diff_lines:
441 443 for line in diff['chunks']:
442 444 _html_empty = False
443 445 for change in line:
444 446 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
445 447 'lc': line_class,
446 448 'action': change['action']
447 449 })
448 450 anchor_old_id = ''
449 451 anchor_new_id = ''
450 452 anchor_old = "%(filename)s_o%(oldline_no)s" % {
451 453 'filename': self._safe_id(diff['filename']),
452 454 'oldline_no': change['old_lineno']
453 455 }
454 456 anchor_new = "%(filename)s_n%(oldline_no)s" % {
455 457 'filename': self._safe_id(diff['filename']),
456 458 'oldline_no': change['new_lineno']
457 459 }
458 460 cond_old = (change['old_lineno'] != '...' and
459 461 change['old_lineno'])
460 462 cond_new = (change['new_lineno'] != '...' and
461 463 change['new_lineno'])
462 464 if cond_old:
463 465 anchor_old_id = 'id="%s"' % anchor_old
464 466 if cond_new:
465 467 anchor_new_id = 'id="%s"' % anchor_new
466 468 ###########################################################
467 469 # OLD LINE NUMBER
468 470 ###########################################################
469 471 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
470 472 'a_id': anchor_old_id,
471 473 'olc': old_lineno_class
472 474 })
473 475
474 476 _html.append('''%(link)s''' % {
475 477 'link': _link_to_if(True, change['old_lineno'],
476 478 '#%s' % anchor_old)
477 479 })
478 480 _html.append('''</td>\n''')
479 481 ###########################################################
480 482 # NEW LINE NUMBER
481 483 ###########################################################
482 484
483 485 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
484 486 'a_id': anchor_new_id,
485 487 'nlc': new_lineno_class
486 488 })
487 489
488 490 _html.append('''%(link)s''' % {
489 491 'link': _link_to_if(True, change['new_lineno'],
490 492 '#%s' % anchor_new)
491 493 })
492 494 _html.append('''</td>\n''')
493 495 ###########################################################
494 496 # CODE
495 497 ###########################################################
496 498 comments = '' if enable_comments else 'no-comment'
497 499 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
498 500 'cc': code_class,
499 501 'inc': comments
500 502 })
501 503 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
502 504 'code': change['line']
503 505 })
504 506 _html.append('''\t</td>''')
505 507 _html.append('''\n</tr>\n''')
506 508 _html.append('''</table>''')
507 509 if _html_empty:
508 510 return None
509 511 return ''.join(_html)
510 512
511 513 def stat(self):
512 514 """
513 515 Returns tuple of added, and removed lines for this instance
514 516 """
515 517 return self.adds, self.removes
General Comments 0
You need to be logged in to leave comments. Login now