#! /usr/bin/python # # 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 # # # Transform a unified diff from stdin to a colored # side-by-side HTML page on stdout. # # Authors: Olivier MATZ # Alan De Smet # Sergey Satskiy # ibex # # Original Python script: http://git.droids-corp.org/gitweb/?p=diff2html # This version: http://patientensicht.ch/node/165 # # Changes 06.01.2013 by ibex: # - Using simplediff (https://github.com/paulgb/simplediff) # - Word by word comparison # - Support UTF-8 # - HTML5 DOCTYPE # - Footer # - Generator and author # - Charset # - Title # - Favicon # # Inspired by diff2html.rb from Dave Burt # (mainly for html theme) # # TODO: # - The sane function currently mashes non-ASCII characters to "." # Instead be clever and convert to something like "xF0" # (the hex value), and mark with a . Even more clever: # Detect if the character is "printable" for whatever definition, # and display those directly. # - Parameter for line diff algorithm selection import sys, re, htmlentitydefs, getopt, StringIO, codecs from simplediff import diff, string_diff # minimum line size, we add a zero-sized breakable space every # LINESIZE characters linesize = 20 tabsize = 4 show_CR = False encoding = "utf-8" html_hdr = """ HTML Diff{0} """ html_footer = """ """ table_hdr = """ """ table_footer = """
""" DIFFON = "\x01" DIFFOFF = "\x02" buf = [] add_cpt, del_cpt = 0, 0 line1, line2 = 0, 0 hunk_off1, hunk_size1, hunk_off2, hunk_size2 = 0, 0, 0, 0 # Characters we're willing to word wrap on WORDBREAK = " \t;.,/):-" def sane(x): r = "" for i in x: j = ord(i) if i not in ['\t', '\n'] and (j < 32): r = r + "." else: r = r + i return r def linediff(s, t): ''' Original line diff algorithm of diff2html. It's character based. ''' if len(s): s = unicode(reduce(lambda x, y:x+y, [ sane(c) for c in s ])) if len(t): t = unicode(reduce(lambda x, y:x+y, [ sane(c) for c in t ])) m, n = len(s), len(t) d = [[(0, 0) for i in range(n+1)] for i in range(m+1)] d[0][0] = (0, (0, 0)) for i in range(m+1)[1:]: d[i][0] = (i,(i-1, 0)) for j in range(n+1)[1:]: d[0][j] = (j,(0, j-1)) for i in range(m+1)[1:]: for j in range(n+1)[1:]: if s[i-1] == t[j-1]: cost = 0 else: cost = 1 d[i][j] = min((d[i-1][j][0] + 1, (i-1, j)), (d[i][j-1][0] + 1, (i, j-1)), (d[i-1][j-1][0] + cost, (i-1, j-1))) l = [] coord = (m, n) while coord != (0, 0): l.insert(0, coord) x, y = coord coord = d[x][y][1] l1 = [] l2 = [] for coord in l: cx, cy = coord child_val = d[cx][cy][0] father_coord = d[cx][cy][1] fx, fy = father_coord father_val = d[fx][fy][0] diff = (cx-fx, cy-fy) if diff == (0, 1): l1.append("") l2.append(DIFFON + t[fy] + DIFFOFF) elif diff == (1, 0): l1.append(DIFFON + s[fx] + DIFFOFF) l2.append("") elif child_val-father_val == 1: l1.append(DIFFON + s[fx] + DIFFOFF) l2.append(DIFFON + t[fy] + DIFFOFF) else: l1.append(s[fx]) l2.append(t[fy]) r1, r2 = (reduce(lambda x, y:x+y, l1), reduce(lambda x, y:x+y, l2)) return r1, r2 def diff_changed(old, new): ''' Returns the differences basend on characters between two strings wrapped with DIFFON and DIFFOFF using `diff`. ''' con = {'=': (lambda x: x), '+': (lambda x: DIFFON + x + DIFFOFF), '-': (lambda x: '')} return "".join([(con[a])("".join(b)) for a, b in diff(old, new)]) def diff_changed_ts(old, new): ''' Returns a tuple for a two sided comparison based on characters, see `diff_changed`. ''' return (diff_changed(new, old), diff_changed(old, new)) def word_diff(old, new): ''' Returns the difference between the old and new strings based on words. Punctuation is not part of the word. Params: old the old string new the new string Returns: the output of `diff` on the two strings after splitting them on whitespace (a list of change instructions; see the docstring of `diff`) ''' separator_pattern = '(\W+)'; return diff(re.split(separator_pattern, old, flags=re.UNICODE), re.split(separator_pattern, new, flags=re.UNICODE)) def diff_changed_words(old, new): ''' Returns the difference between two strings based on words (see `word_diff`) wrapped with DIFFON and DIFFOFF. Returns: the output of the diff expressed delimited with DIFFON and DIFFOFF. ''' con = {'=': (lambda x: x), '+': (lambda x: DIFFON + x + DIFFOFF), '-': (lambda x: '')} return "".join([(con[a])("".join(b)) for a, b in word_diff(old, new)]) def diff_changed_words_ts(old, new): ''' Returns a tuple for a two sided comparison based on words, see `diff_changed_words`. ''' return (diff_changed_words(new, old), diff_changed_words(old, new)) def convert(s, linesize=0, ponct=0): i = 0 t = "" for c in s: # used by diffs if c == DIFFON: t += '' elif c == DIFFOFF: t += "" # special html chars elif htmlentitydefs.codepoint2name.has_key(ord(c)): t += "&%s;" % (htmlentitydefs.codepoint2name[ord(c)]) i += 1 # special highlighted chars elif c == "\t" and ponct == 1: n = tabsize-(i%tabsize) if n == 0: n = tabsize t += ('»'+' '*(n-1)) elif c == " " and ponct == 1: t += '·' elif c == "\n" and ponct == 1: if show_CR: t += '\' else: t += c i += 1 if linesize and (WORDBREAK.count(c) == 1): t += '​' i = 0 if linesize and i > linesize: i = 0 t += "​" return t.encode(encoding) def add_comment(s, output_file): output_file.write('%s\n'%convert(s)) def add_filename(f1, f2, output_file): output_file.write("%s"%convert(f1, linesize=linesize)) output_file.write("%s\n"%convert(f2, linesize=linesize)) def add_hunk(output_file, show_hunk_infos): if show_hunk_infos: output_file.write('Offset %d, %d lines modified'%(hunk_off1, hunk_size1)) output_file.write('Offset %d, %d lines modified\n'%(hunk_off2, hunk_size2)) else: # ⋮ - vertical ellipsis output_file.write('⋮⋮') def add_line(s1, s2, output_file): global line1 global line2 orig1 = s1 orig2 = s2 if s1 == None and s2 == None: type_name = "unmodified" elif s1 == None or s1 == "": type_name = "added" elif s2 == None or s1 == "": type_name = "deleted" elif s1 == s2: type_name = "unmodified" else: type_name = "changed" s1, s2 = diff_changed_words_ts(orig1, orig2) # s1b, s2b = diff_changed_ts(orig1, orig2) # s1c, s2c = linediff(orig1, orig2) output_file.write(('' % type_name).encode(encoding)) if s1 != None and s1 != "": output_file.write(('%d ' % line1).encode(encoding)) output_file.write(''.encode(encoding)) output_file.write(convert(s1, linesize=linesize, ponct=1)) output_file.write('') else: s1 = "" output_file.write(' ') if s2 != None and s2 != "": output_file.write(('%d '%line2).encode(encoding)) output_file.write('') output_file.write(convert(s2, linesize=linesize, ponct=1)) output_file.write('') else: s2 = "" output_file.write('') output_file.write('\n') # Output all for a line diff algorithm comparison #if (False and type_name == "changed"): #output_file.write(('' % type_name).encode(encoding)) #if s1 != None and s1 != "": #output_file.write(('%d ' % line1).encode(encoding)) #output_file.write(''.encode(encoding)) #output_file.write(convert(s1b, linesize=linesize, ponct=1)) #output_file.write('') #else: #s1 = "" #output_file.write(' ') #if s2 != None and s2 != "": #output_file.write(('%d '%line2).encode(encoding)) #output_file.write('') #output_file.write(convert(s2b, linesize=linesize, ponct=1)) #output_file.write('') #else: #s2 = "" #output_file.write('') #output_file.write(('' % type_name).encode(encoding)) #if s1 != None and s1 != "": #output_file.write(('%d ' % line1).encode(encoding)) #output_file.write(''.encode(encoding)) #output_file.write(convert(s1c, linesize=linesize, ponct=1)) #output_file.write('') #else: #s1 = "" #output_file.write(' ') #if s2 != None and s2 != "": #output_file.write(('%d '%line2).encode(encoding)) #output_file.write('') #output_file.write(convert(s2c, linesize=linesize, ponct=1)) #output_file.write('') #else: #s2 = "" #output_file.write('') #output_file.write('\n') if s1 != "": line1 += 1 if s2 != "": line2 += 1 def empty_buffer(output_file): global buf global add_cpt global del_cpt if del_cpt == 0 or add_cpt == 0: for l in buf: add_line(l[0], l[1], output_file) elif del_cpt != 0 and add_cpt != 0: l0, l1 = [], [] for l in buf: if l[0] != None: l0.append(l[0]) if l[1] != None: l1.append(l[1]) max_len = (len(l0) > len(l1)) and len(l0) or len(l1) for i in range(max_len): s0, s1 = "", "" if i < len(l0): s0 = l0[i] if i < len(l1): s1 = l1[i] add_line(s0, s1, output_file) add_cpt, del_cpt = 0, 0 buf = [] def parse_input(input_file, output_file, input_file_name, output_file_name, exclude_headers, show_hunk_infos): global add_cpt, del_cpt global line1, line2 global hunk_off1, hunk_size1, hunk_off2, hunk_size2 if not exclude_headers: title_suffix = ' ' + input_file_name output_file.write(html_hdr.format(title_suffix, encoding).encode(encoding)) output_file.write(table_hdr.encode(encoding)) while True: l = input_file.readline() if l == "": break m = re.match('^--- ([^\s]*)', l) if m: empty_buffer(output_file) file1 = m.groups()[0] while True: l = input_file.readline() m = re.match('^\+\+\+ ([^\s]*)', l) if m: file2 = m.groups()[0] break add_filename(file1, file2, output_file) hunk_off1, hunk_size1, hunk_off2, hunk_size2 = 0, 0, 0, 0 continue m = re.match("@@ -(\d+),?(\d*) \+(\d+),?(\d*)", l) if m: empty_buffer(output_file) hunk_data = map(lambda x:x=="" and 1 or int(x), m.groups()) hunk_off1, hunk_size1, hunk_off2, hunk_size2 = hunk_data line1, line2 = hunk_off1, hunk_off2 add_hunk(output_file, show_hunk_infos) continue if hunk_size1 == 0 and hunk_size2 == 0: empty_buffer(output_file) add_comment(l, output_file) continue if re.match("^\+", l): add_cpt += 1 hunk_size2 -= 1 buf.append((None, l[1:])) continue if re.match("^\-", l): del_cpt += 1 hunk_size1 -= 1 buf.append((l[1:], None)) continue if re.match("^\ ", l) and hunk_size1 and hunk_size2: empty_buffer(output_file) hunk_size1 -= 1 hunk_size2 -= 1 buf.append((l[1:], l[1:])) continue empty_buffer(output_file) add_comment(l, output_file) empty_buffer(output_file) output_file.write(table_footer.encode(encoding)) if not exclude_headers: output_file.write(html_footer.encode(encoding)) def usage(): print ''' diff2html.py [-e encoding] [-i file] [-o file] [-x] diff2html.py -h Transform a unified diff from stdin to a colored side-by-side HTML page on stdout. -i file set input file, else use stdin -e encoding set file encoding (default utf-8) -o file set output file, else use stdout -x exclude html header and footer -t tabsize set tab size (default 8) -l linesize set maximum line size is there is no word break (default 20) -r show \\r characters -k show hunk infos -h show help and exit ''' def main(): global linesize, tabsize global show_CR global encoding input_file = sys.stdin output_file = sys.stdout input_file_name = '' output_file_name = '' exclude_headers = False show_hunk_infos = False try: opts, args = getopt.getopt(sys.argv[1:], "he:i:o:xt:l:rk", ["help", "encoding=", "input=", "output=", "exclude-html-headers", "tabsize=", "linesize=", "show-cr", "show-hunk-infos"]) except getopt.GetoptError, err: print unicode(err) # will print something like "option -a not recognized" usage() sys.exit(2) verbose = False for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() elif o in ("-e", "--encoding"): encoding = a elif o in ("-i", "--input"): input_file = codecs.open(a, "r", encoding) input_file_name = a elif o in ("-o", "--output"): output_file = codecs.open(a, "w") output_file_name = a elif o in ("-x", "--exclude-html-headers"): exclude_headers = True elif o in ("-t", "--tabsize"): tabsize = int(a) elif o in ("-l", "--linesize"): linesize = int(a) elif o in ("-r", "--show-cr"): show_CR = True elif o in ("-k", "--show-hunk-infos"): show_hunk_infos = True else: assert False, "unhandled option" parse_input(input_file, output_file, input_file_name, output_file_name, exclude_headers, show_hunk_infos) def parse_from_memory(txt, exclude_headers, show_hunk_infos): " Parses diff from memory and returns a string with html " input_stream = StringIO.StringIO(txt) output_stream = StringIO.StringIO() parse_input(input_stream, output_stream, '', '', exclude_headers, show_hunk_infos) return output_stream.getvalue() if __name__ == "__main__": main()