123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- #!/usr/bin/env python
-
- import sys
- import os
- import re
-
-
- class Hunk(object):
-
- def __init__(self, hunk_header, old_addr, old_offset, new_addr, new_offset):
- self.__hunk_header = hunk_header
- self.__old_addr = old_addr
- self.__old_offset = old_offset
- self.__new_addr = new_addr
- self.__new_offset = new_offset
- self.__hunk_list = [] # 2-element group (attr, line)
-
- def get_header(self):
- return self.__hunk_header
-
- def get_old_addr(self):
- return (self.__old_addr, self.__old_offset)
-
- def get_new_addr(self):
- return (self.__new_addr, self.__new_offset)
-
- def append(self, attr, line):
- """attr: '-': old, '+': new, ' ': common"""
- self.__hunk_list.append((attr, line))
-
- def __iter__(self):
- for hunk_line in self.__hunk_list:
- yield hunk_line
-
-
- class Diff(object):
-
- def __init__(self, headers, old_path, new_path, hunks):
- self.__headers = headers
- self.__old_path = old_path
- self.__new_path = new_path
- self.__hunks = hunks
-
- def render_traditional(self, show_color):
- out = []
- if show_color:
- color = None # Use default
- end_color = None
- else:
- color = 'none' # No color
- end_color = 'none'
-
- for line in self.__headers:
- out.append(self._render_header(line, color, end_color))
-
- out.append(self._render_old_path(self.__old_path, color, end_color))
- out.append(self._render_new_path(self.__new_path, color, end_color))
-
- for hunk in self.__hunks:
- out.append(self._render_hunk_header(hunk.get_header(), color,
- end_color))
- for (attr, line) in hunk:
- if attr == '-':
- out.append(self._render_old(attr+line, color, end_color))
- elif attr == '+':
- out.append(self._render_new(attr+line, color, end_color))
- else:
- out.append(self._render_common(attr+line, color, end_color))
-
- return ''.join(out)
-
- def render_side_by_side(self, show_color, show_number, width):
- """Do not really need to parse the hunks..."""
- return 'TODO: show_color=%s, show_number=%s, width=%d' % (show_color,
- show_number, width)
-
- def _render_header(self, line, color=None, end_color=None):
- if color is None:
- color='cyan'
- if end_color is None:
- end_color = 'reset'
- return self.__mark_color(line, color, end_color)
-
- def _render_old_path(self, line, color=None, end_color=None):
- if color is None:
- color='yellow'
- if end_color is None:
- end_color = 'reset'
- return self.__mark_color(line, color, end_color)
-
- def _render_new_path(self, line, color=None, end_color=None):
- if color is None:
- color='yellow'
- if end_color is None:
- end_color = 'reset'
- return self.__mark_color(line, color, end_color)
-
- def _render_hunk_header(self, line, color=None, end_color=None):
- if color is None:
- color='lightblue'
- if end_color is None:
- end_color = 'reset'
- return self.__mark_color(line, color, end_color)
-
- def _render_old(self, line, color=None, end_color=None):
- if color is None:
- color='red'
- if end_color is None:
- end_color = 'reset'
- return self.__mark_color(line, color, end_color)
-
- def _render_new(self, line, color=None, end_color=None):
- if color is None:
- color='green'
- if end_color is None:
- end_color = 'reset'
- return self.__mark_color(line, color, end_color)
-
- def _render_common(self, line, color=None, end_color=None):
- if color is None:
- color='none'
- if end_color is None:
- end_color = 'none'
- return self.__mark_color(line, color, end_color)
-
- def __mark_color(self, text, start_code, end_code):
- colors = {
- 'none' : '',
- 'reset' : '\x1b[0m',
- 'red' : '\x1b[31m',
- 'green' : '\x1b[32m',
- 'yellow' : '\x1b[33m',
- 'blue' : '\x1b[34m',
- 'cyan' : '\x1b[36m',
- 'lightblue' : '\x1b[1;34m',
- }
- return colors.get(start_code) + text + colors.get(end_code)
-
-
- class Udiff(Diff):
-
- @staticmethod
- def is_old_path(line):
- return line.startswith('--- ')
-
- @staticmethod
- def is_new_path(line):
- return line.startswith('+++ ')
-
- @staticmethod
- def is_hunk_header(line):
- return line.startswith('@@ -')
-
- @staticmethod
- def is_old(line):
- return line.startswith('-') and not Udiff.is_old_path(line)
-
- @staticmethod
- def is_new(line):
- return line.startswith('+') and not Udiff.is_new_path(line)
-
- @staticmethod
- def is_common(line):
- return line.startswith(' ')
-
- @staticmethod
- def is_eof(line):
- # \ No newline at end of file
- return line.startswith('\\')
-
- @staticmethod
- def is_header(line):
- return re.match(r'^[^+@\\ -]', line)
-
-
- class DiffParser(object):
-
- def __init__(self, stream):
- for line in stream[:10]:
- if line.startswith('+++ '):
- self.__type = 'udiff'
- break
- else:
- raise RuntimeError('unknown diff type')
-
- try:
- self.__diffs = self.__parse(stream)
- except (AssertionError, IndexError):
- raise RuntimeError('invalid patch format')
-
-
- def get_diffs(self):
- return self.__diffs
-
- def __parse(self, stream):
- if self.__type == 'udiff':
- return self.__parse_udiff(stream)
- else:
- raise RuntimeError('unsupported diff format')
-
- def __parse_udiff(self, stream):
- """parse all diff lines here, construct a list of Udiff objects"""
- out_diffs = []
- headers = []
- old_path = None
- new_path = None
- hunks = []
- hunk = None
-
- while stream:
- if Udiff.is_header(stream[0]):
- if headers and old_path:
- # Encounter a new header
- assert new_path is not None
- assert hunk is not None
- hunks.append(hunk)
- out_diffs.append(Diff(headers, old_path, new_path, hunks))
- headers = []
- old_path = None
- new_path = None
- hunks = []
- hunk = None
- else:
- headers.append(stream.pop(0))
-
- elif Udiff.is_old_path(stream[0]):
- if old_path:
- # Encounter a new patch set
- assert new_path is not None
- assert hunk is not None
- hunks.append(hunk)
- out_diffs.append(Diff(headers, old_path, new_path, hunks))
- headers = []
- old_path = None
- new_path = None
- hunks = []
- hunk = None
- else:
- old_path = stream.pop(0)
-
- elif Udiff.is_new_path(stream[0]):
- assert old_path is not None
- assert new_path is None
- new_path = stream.pop(0)
-
- elif Udiff.is_hunk_header(stream[0]):
- assert old_path is not None
- assert new_path is not None
- if hunk:
- # Encounter a new hunk header
- hunks.append(hunk)
- hunk = None
- else:
- # @@ -3,7 +3,6 @@
- hunk_header = stream.pop(0)
-
- addr_info = hunk_header.split()[1]
- assert addr_info.startswith('-')
- old_addr = addr_info.split(',')[0]
- old_offset = addr_info.split(',')[1]
-
- addr_info = hunk_header.split()[2]
- assert addr_info.startswith('+')
- new_addr = addr_info.split(',')[0]
- new_offset = addr_info.split(',')[1]
-
- hunk = Hunk(hunk_header, old_addr, old_offset, new_addr,
- new_offset)
-
- elif Udiff.is_old(stream[0]) or Udiff.is_new(stream[0]) or \
- Udiff.is_common(stream[0]):
- assert old_path is not None
- assert new_path is not None
- assert hunk is not None
- hunk_line = stream.pop(0)
- hunk.append(hunk_line[0], hunk_line[1:])
-
- elif Udiff.is_eof(stream[0]):
- # ignore
- stream.pop(0)
-
- else:
- raise RuntimeError('unknown patch format: %s' % stream[0])
-
- # The last patch
- if hunk:
- hunks.append(hunk)
- if old_path:
- if new_path:
- out_diffs.append(Diff(headers, old_path, new_path, hunks))
- else:
- raise RuntimeError('unknown patch format after "%s"' % old_path)
- elif headers:
- raise RuntimeError('unknown patch format: %s' % \
- ('\n'.join(headers)))
-
- return out_diffs
-
-
- class DiffRender(object):
-
- def __init__(self, stream):
- self.__diffs = DiffParser(stream).get_diffs()
-
- def render(self, show_color=True, show_number=False, width=0,
- traditional=False):
- if traditional:
- return self.__render_traditional(show_color)
- else:
- return self.__render_side_by_side(show_color, show_number, width)
-
- def __render_traditional(self, show_color):
- out = []
- for diff in self.__diffs:
- out.append(diff.render_traditional(show_color))
- return out
-
- def __render_side_by_side(self, show_color, show_number, width):
- """width of 0 or negative means auto detect terminal width"""
- out = []
- for diff in self.__diffs:
- out.append(diff.render_side_by_side(show_color, show_number, width))
- return out
-
-
- if __name__ == '__main__':
- import optparse
- import subprocess
-
- usage = '''
- %(prog)s [options] [diff]
-
- View diff (patch) file if given, otherwise read stdin''' % \
- {'prog': os.path.basename(sys.argv[0])}
-
- parser = optparse.OptionParser(usage)
- parser.add_option('-n', '--number', action='store_true',
- help='show line number')
- parser.add_option('-w', '--width', type='int', default=0,
- help='set text width for each side')
- parser.add_option('-t', '--traditional', action='store_true',
- help=('show in traditional format other than default side-by-side '
- 'mode (omit -n, -w)'))
- opts, args = parser.parse_args()
-
- show_color = sys.stdout.isatty()
-
- if opts.width < 0:
- opts.width = 0
-
- if len(args) >= 1:
- diff_hdl = open(args[0], 'r')
- elif sys.stdin.isatty():
- sys.stderr.write('Try --help option for usage\n')
- sys.exit(1)
- else:
- diff_hdl = sys.stdin
-
- stream = diff_hdl.readlines()
- if diff_hdl is not sys.stdin:
- diff_hdl.close()
-
- render = DiffRender(stream)
- color_diff = render.render(show_color=show_color, show_number=opts.number,
- width=opts.width, traditional=opts.traditional)
-
- if sys.stdout.isatty():
- # args stolen fron git source: github.com/git/git/blob/master/pager.c
- pager = subprocess.Popen(['less', '-FRSXK'],
- stdin=subprocess.PIPE, stdout=sys.stdout)
- pager.stdin.write(''.join(color_diff))
- pager.stdin.close()
- pager.wait()
- else:
- sys.stdout.write(''.join(color_diff))
-
- sys.exit(0)
-
- # vim:set et sts=4 sw=4 tw=80:
|