From 593d9e4c5a4c770c968d1d9395065a78f58f571a Mon Sep 17 00:00:00 2001 From: Rafael Monnerat <rafael@nexedi.com> Date: Thu, 4 Feb 2010 19:26:18 +0000 Subject: [PATCH] Provide a feature not present into difflib, which is generate a colored diff from a diff file/string. This code is original form ERP5Subversion and was moved to here for be used in general ERP5. XXX The organisation of DiffUtils should be reviewed and reorganised if needed. ie.: Move behaviour to a tool. git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@32272 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5Type/DiffUtils.py | 302 ++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 product/ERP5Type/DiffUtils.py diff --git a/product/ERP5Type/DiffUtils.py b/product/ERP5Type/DiffUtils.py new file mode 100644 index 0000000000..4902c98ef5 --- /dev/null +++ b/product/ERP5Type/DiffUtils.py @@ -0,0 +1,302 @@ +############################################################################## +# +# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved. +# Yoshinori Okuji <yo@nexedi.com> +# Christophe Dumez <christophe@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +""" + Provide a feature not present into difflib, which is generate a colored diff + from a diff file/string. + + This code is original form ERP5Subversion and was moved to here for be used in + general ERP5. + + XXX The organisation of DiffUtils should be reviewed and reorganised in a tool + if a general tool want to be provided. +""" +import os, re +from xml.sax.saxutils import escape + +NBSP = ' ' +NBSP_TAB = NBSP*8 + +class DiffFile: + """ + # Members : + - path : path of the modified file + - children : sub codes modified + - old_revision + - new_revision + """ + + def __init__(self, raw_diff): + if '@@' not in raw_diff: + self.binary = True + return + else: + self.binary = False + self.header = raw_diff.split('@@')[0][:-1] + # Getting file path in header + self.path = self.header.split('====')[0][:-1].strip() + # Getting revisions in header + for line in self.header.splitlines(): + if line.startswith('--- '): + tmp = re.search('\\([^)]+\\)$', line) + self.old_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip() + if line.startswith('+++ '): + tmp = re.search('\\([^)]+\\)$', line) + self.new_revision = tmp.string[tmp.start():tmp.end()][1:-1].strip() + # Splitting the body from the header + self.body = os.linesep.join(raw_diff.strip().splitlines()[4:]) + # Now splitting modifications + self.children = [] + first = True + tmp = [] + for line in self.body.splitlines(): + if line: + if line.startswith('@@') and not first: + self.children.append(CodeBlock(os.linesep.join(tmp))) + tmp = [line, ] + else: + first = False + tmp.append(line) + self.children.append(CodeBlock(os.linesep.join(tmp))) + + def toHTML(self): + """ return HTML diff + """ + # Adding header of the table + if self.binary: + return '<b>Folder or binary file or just no changes!</b><br/><br/><br/>' + + html_list = [] + html_list.append(''' + <table style="text-align: left; width: 100%%; border: 0;" cellpadding="0" cellspacing="0"> + <tbody> + <tr> + <td style="background-color: grey; text-align: center; font-weight: bold;">%s</td> + <td style="background-color: black; width: 2px;"></td> + <td style="background-color: grey; text-align: center; font-weight: bold;">%s</td> + </tr>''' % (self.old_revision, self.new_revision)) + header_color = 'grey' + child_html_text = '''<tr><td style="background-color: %(headcolor)s"> + </td><td style="background-color: black; width: 2px;"></td> + <td style="background-color: %(headcolor)s"> </td></tr><tr> + <td style="background-color: rgb(68, 132, 255);font-weight: bold;">Line %(oldline)s</td> + <td style="background-color: black; width: 2px;"></td> + <td style="background-color: rgb(68, 132, 255);font-weight: bold;">Line %(newline)s</td> + </tr>''' + for child in self.children: + # Adding line number of the modification + html_list.append( child_html_text % {'headcolor':header_color, 'oldline':child.old_line, 'newline':child.new_line} ) + header_color = 'white' + # Adding diff of the modification + old_code_list = child.getOldCodeList() + new_code_list = child.getNewCodeList() + i = 0 + for old_line_tuple in old_code_list: + new_line_tuple = new_code_list[i] + new_line = new_line_tuple[0] or ' ' + old_line = old_line_tuple[0] or ' ' + i += 1 + html_list.append( '''<tr> + <td style="background-color: %s">%s</td> + <td style="background-color: black; width: 2px;"></td> + <td style="background-color: %s">%s</td> + </tr>'''%(old_line_tuple[1], + escape(old_line).replace(' ', NBSP).replace('\t', NBSP_TAB), + new_line_tuple[1], + escape(new_line).replace(' ', NBSP).replace('\t', NBSP_TAB)) + ) + html_list.append('''</tbody></table><br/>''') + return '\n'.join(html_list) + +class CodeBlock: + """ + A code block contains several SubCodeBlocks + Members : + - old_line : line in old code (before modif) + - new line : line in new code (after modif) + + Methods : + - getOldCodeList() : return code before modif + - getNewCodeList() : return code after modif + Note: the code returned is a list of tuples (code line, background color) + """ + + def __init__(self, raw_diff): + # Splitting body and header + self.body = os.linesep.join(raw_diff.splitlines()[1:]) + self.header = raw_diff.splitlines()[0] + # Getting modifications lines + tmp = re.search('^@@ -\d+', self.header) + self.old_line = tmp.string[tmp.start():tmp.end()][4:] + tmp = re.search('\+\d+', self.header) + self.new_line = tmp.string[tmp.start():tmp.end()][1:] + # Splitting modifications in SubCodeBlocks + in_modif = False + self.children = [] + tmp = [] + for line in self.body.splitlines(): + if line: + if (line.startswith('+') or line.startswith('-')): + if in_modif: + tmp.append(line) + else: + self.children.append(SubCodeBlock(os.linesep.join(tmp))) + tmp = [line, ] + in_modif = True + else: + if in_modif: + self.children.append(SubCodeBlock(os.linesep.join(tmp))) + tmp = [line, ] + in_modif = False + else: + tmp.append(line) + self.children.append(SubCodeBlock(os.linesep.join(tmp))) + + def getOldCodeList(self): + """ Return code before modification + """ + tmp = [] + for child in self.children: + tmp.extend(child.getOldCodeList()) + return tmp + + def getNewCodeList(self): + """ Return code after modification + """ + tmp = [] + for child in self.children: + tmp.extend(child.getNewCodeList()) + return tmp + +class SubCodeBlock: + """ a SubCodeBlock contain 0 or 1 modification (not more) + """ + def __init__(self, code): + self.body = code + self.modification = self._getModif() + self.old_code_length = self._getOldCodeLength() + self.new_code_length = self._getNewCodeLength() + # Choosing background color + if self.modification == 'none': + self.color = 'white' + elif self.modification == 'change': + self.color = 'rgb(253, 228, 6);'#light orange + elif self.modification == 'deletion': + self.color = 'rgb(253, 117, 74);'#light red + else: # addition + self.color = 'rgb(83, 253, 74);'#light green + + def _getModif(self): + """ Return type of modification : + addition, deletion, none + """ + nb_plus = 0 + nb_minus = 0 + for line in self.body.splitlines(): + if line.startswith("-"): + nb_minus -= 1 + elif line.startswith("+"): + nb_plus += 1 + if (nb_plus == 0 and nb_minus == 0): + return 'none' + if (nb_minus == 0): + return 'addition' + if (nb_plus == 0): + return 'deletion' + return 'change' + + def _getOldCodeLength(self): + """ Private function to return old code length + """ + nb_lines = 0 + for line in self.body.splitlines(): + if not line.startswith("+"): + nb_lines += 1 + return nb_lines + + def _getNewCodeLength(self): + """ Private function to return new code length + """ + nb_lines = 0 + for line in self.body.splitlines(): + if not line.startswith("-"): + nb_lines += 1 + return nb_lines + + def getOldCodeList(self): + """ Return code before modification + """ + if self.modification == 'none': + old_code = [(x, 'white') for x in self.body.splitlines()] + elif self.modification == 'change': + old_code = [self._getOldCodeList(x) for x in self.body.splitlines() \ + if self._getOldCodeList(x)[0]] + # we want old_code_list and new_code_list to have the same length + if(self.old_code_length < self.new_code_length): + filling = [(None, self.color)] * (self.new_code_length - \ + self.old_code_length) + old_code.extend(filling) + else: # deletion or addition + old_code = [self._getOldCodeList(x) for x in self.body.splitlines()] + return old_code + + def _getOldCodeList(self, line): + """ Private function to return code before modification + """ + if line.startswith('+'): + return (None, self.color) + if line.startswith('-'): + return (' ' + line[1:], self.color) + return (line, self.color) + + def getNewCodeList(self): + """ Return code after modification + """ + if self.modification == 'none': + new_code = [(x, 'white') for x in self.body.splitlines()] + elif self.modification == 'change': + new_code = [self._getNewCodeList(x) for x in self.body.splitlines() \ + if self._getNewCodeList(x)[0]] + # we want old_code_list and new_code_list to have the same length + if(self.new_code_length < self.old_code_length): + filling = [(None, self.color)] * (self.old_code_length - \ + self.new_code_length) + new_code.extend(filling) + else: # deletion or addition + new_code = [self._getNewCodeList(x) for x in self.body.splitlines()] + return new_code + + def _getNewCodeList(self, line): + """ Private function to return code after modification + """ + if line.startswith('-'): + return (None, self.color) + if line.startswith('+'): + return (' ' + line[1:], self.color) + return (line, self.color) -- 2.30.9