cdiff.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. #!/usr/bin/env python
  2. import sys
  3. import os
  4. import re
  5. class Hunk(object):
  6. def __init__(self, hunk_header, old_addr, old_offset, new_addr, new_offset):
  7. self.__hunk_header = hunk_header
  8. self.__old_addr = old_addr
  9. self.__old_offset = old_offset
  10. self.__new_addr = new_addr
  11. self.__new_offset = new_offset
  12. self.__hunk_list = [] # 2-element group (attr, line)
  13. def get_header(self):
  14. return self.__hunk_header
  15. def get_old_addr(self):
  16. return (self.__old_addr, self.__old_offset)
  17. def get_new_addr(self):
  18. return (self.__new_addr, self.__new_offset)
  19. def append(self, attr, line):
  20. """attr: '-': old, '+': new, ' ': common"""
  21. self.__hunk_list.append((attr, line))
  22. def __iter__(self):
  23. for hunk_line in self.__hunk_list:
  24. yield hunk_line
  25. class Diff(object):
  26. def __init__(self, headers, old_path, new_path, hunks):
  27. self.__headers = headers
  28. self.__old_path = old_path
  29. self.__new_path = new_path
  30. self.__hunks = hunks
  31. def view_traditional(self, show_color):
  32. out = []
  33. if show_color:
  34. color = None # Use default
  35. end_color = None
  36. else:
  37. color = 'none' # No color
  38. end_color = 'none'
  39. for line in self.__headers:
  40. out.append(self._view_header(line, color, end_color))
  41. out.append(self._view_old_path(self.__old_path, color, end_color))
  42. out.append(self._view_new_path(self.__new_path, color, end_color))
  43. for hunk in self.__hunks:
  44. out.append(self._view_hunk_header(hunk.get_header(), color,
  45. end_color))
  46. for (attr, line) in hunk:
  47. if attr == '-':
  48. out.append(self._view_old(attr+line, color, end_color))
  49. elif attr == '+':
  50. out.append(self._view_new(attr+line, color, end_color))
  51. else:
  52. out.append(self._view_common(attr+line, color, end_color))
  53. return ''.join(out)
  54. def view_side_by_side(self, show_color, show_number, width):
  55. """Do not really need to parse the hunks..."""
  56. return 'TODO: show_color=%s, show_number=%s, width=%d' % (show_color,
  57. show_number, width)
  58. def _view_header(self, line, color=None, end_color=None):
  59. if color is None:
  60. color='cyan'
  61. if end_color is None:
  62. end_color = 'reset'
  63. return self.__mark_color(line, color, end_color)
  64. def _view_old_path(self, line, color=None, end_color=None):
  65. if color is None:
  66. color='yellow'
  67. if end_color is None:
  68. end_color = 'reset'
  69. return self.__mark_color(line, color, end_color)
  70. def _view_new_path(self, line, color=None, end_color=None):
  71. if color is None:
  72. color='yellow'
  73. if end_color is None:
  74. end_color = 'reset'
  75. return self.__mark_color(line, color, end_color)
  76. def _view_hunk_header(self, line, color=None, end_color=None):
  77. if color is None:
  78. color='lightblue'
  79. if end_color is None:
  80. end_color = 'reset'
  81. return self.__mark_color(line, color, end_color)
  82. def _view_old(self, line, color=None, end_color=None):
  83. if color is None:
  84. color='red'
  85. if end_color is None:
  86. end_color = 'reset'
  87. return self.__mark_color(line, color, end_color)
  88. def _view_new(self, line, color=None, end_color=None):
  89. if color is None:
  90. color='green'
  91. if end_color is None:
  92. end_color = 'reset'
  93. return self.__mark_color(line, color, end_color)
  94. def _view_common(self, line, color=None, end_color=None):
  95. if color is None:
  96. color='none'
  97. if end_color is None:
  98. end_color = 'none'
  99. return self.__mark_color(line, color, end_color)
  100. def __mark_color(self, text, start_code, end_code):
  101. colors = {
  102. 'none' : '',
  103. 'reset' : '\x1b[0m',
  104. 'red' : '\x1b[31m',
  105. 'green' : '\x1b[32m',
  106. 'yellow' : '\x1b[33m',
  107. 'blue' : '\x1b[34m',
  108. 'cyan' : '\x1b[36m',
  109. 'lightblue' : '\x1b[1;34m',
  110. }
  111. return colors.get(start_code) + text + colors.get(end_code)
  112. class Udiff(Diff):
  113. @staticmethod
  114. def is_old_path(line):
  115. return line.startswith('--- ')
  116. @staticmethod
  117. def is_new_path(line):
  118. return line.startswith('+++ ')
  119. @staticmethod
  120. def is_hunk_header(line):
  121. return line.startswith('@@ -')
  122. @staticmethod
  123. def is_old(line):
  124. return line.startswith('-') and not Udiff.is_old_path(line)
  125. @staticmethod
  126. def is_new(line):
  127. return line.startswith('+') and not Udiff.is_new_path(line)
  128. @staticmethod
  129. def is_common(line):
  130. return line.startswith(' ')
  131. @staticmethod
  132. def is_eof(line):
  133. # \ No newline at end of file
  134. return line.startswith('\\')
  135. @staticmethod
  136. def is_header(line):
  137. return re.match(r'^[^+@\\ -]', line)
  138. class DiffParser(object):
  139. def __init__(self, stream):
  140. for line in stream[:10]:
  141. if line.startswith('+++ '):
  142. self.__type = 'udiff'
  143. break
  144. else:
  145. raise RuntimeError('unknown diff type')
  146. try:
  147. self.__diffs = self.__parse(stream)
  148. except (AssertionError, IndexError):
  149. raise RuntimeError('invalid patch format')
  150. def get_diffs(self):
  151. return self.__diffs
  152. def __parse(self, stream):
  153. if self.__type == 'udiff':
  154. return self.__parse_udiff(stream)
  155. else:
  156. raise RuntimeError('unsupported diff format')
  157. def __parse_udiff(self, stream):
  158. """parse all diff lines here, construct a list of Udiff objects"""
  159. out_diffs = []
  160. headers = []
  161. old_path = None
  162. new_path = None
  163. hunks = []
  164. hunk = None
  165. while stream:
  166. if Udiff.is_header(stream[0]):
  167. if headers and old_path:
  168. # Encounter a new header
  169. assert new_path is not None
  170. assert hunk is not None
  171. hunks.append(hunk)
  172. out_diffs.append(Diff(headers, old_path, new_path, hunks))
  173. headers = []
  174. old_path = None
  175. new_path = None
  176. hunks = []
  177. hunk = None
  178. else:
  179. headers.append(stream.pop(0))
  180. elif Udiff.is_old_path(stream[0]):
  181. if old_path:
  182. # Encounter a new patch set
  183. assert new_path is not None
  184. assert hunk is not None
  185. hunks.append(hunk)
  186. out_diffs.append(Diff(headers, old_path, new_path, hunks))
  187. headers = []
  188. old_path = None
  189. new_path = None
  190. hunks = []
  191. hunk = None
  192. else:
  193. old_path = stream.pop(0)
  194. elif Udiff.is_new_path(stream[0]):
  195. assert old_path is not None
  196. assert new_path is None
  197. new_path = stream.pop(0)
  198. elif Udiff.is_hunk_header(stream[0]):
  199. assert old_path is not None
  200. assert new_path is not None
  201. if hunk:
  202. # Encounter a new hunk header
  203. hunks.append(hunk)
  204. hunk = None
  205. else:
  206. # @@ -3,7 +3,6 @@
  207. hunk_header = stream.pop(0)
  208. addr_info = hunk_header.split()[1]
  209. assert addr_info.startswith('-')
  210. old_addr = addr_info.split(',')[0]
  211. old_offset = addr_info.split(',')[1]
  212. addr_info = hunk_header.split()[2]
  213. assert addr_info.startswith('+')
  214. new_addr = addr_info.split(',')[0]
  215. new_offset = addr_info.split(',')[1]
  216. hunk = Hunk(hunk_header, old_addr, old_offset, new_addr,
  217. new_offset)
  218. elif Udiff.is_old(stream[0]) or Udiff.is_new(stream[0]) or \
  219. Udiff.is_common(stream[0]):
  220. assert old_path is not None
  221. assert new_path is not None
  222. assert hunk is not None
  223. hunk_line = stream.pop(0)
  224. hunk.append(hunk_line[0], hunk_line[1:])
  225. elif Udiff.is_eof(stream[0]):
  226. # ignore
  227. stream.pop(0)
  228. else:
  229. raise RuntimeError('unknown patch format: %s' % stream[0])
  230. # The last patch
  231. if hunk:
  232. hunks.append(hunk)
  233. if old_path:
  234. if new_path:
  235. out_diffs.append(Diff(headers, old_path, new_path, hunks))
  236. else:
  237. raise RuntimeError('unknown patch format after "%s"' % old_path)
  238. elif headers:
  239. raise RuntimeError('unknown patch format: %s' % \
  240. ('\n'.join(headers)))
  241. return out_diffs
  242. class DiffViewer(object):
  243. def __init__(self, stream):
  244. self.__diffs = DiffParser(stream).get_diffs()
  245. def view(self, show_color=True, show_number=False, width=0,
  246. traditional=False):
  247. if traditional:
  248. return self.__view_traditional(show_color)
  249. else:
  250. return self.__view_side_by_side(show_color, show_number, width)
  251. def __view_traditional(self, show_color):
  252. out = []
  253. for diff in self.__diffs:
  254. out.append(diff.view_traditional(show_color))
  255. return out
  256. def __view_side_by_side(self, show_color, show_number, width):
  257. """width of 0 or negative means auto detect terminal width"""
  258. out = []
  259. for diff in self.__diffs:
  260. out.append(diff.view_side_by_side(show_color, show_number, width))
  261. return out
  262. if __name__ == '__main__':
  263. import optparse
  264. import subprocess
  265. usage = '''
  266. %(prog)s [options] [diff]
  267. View diff (patch) file if given, otherwise read stdin''' % \
  268. {'prog': os.path.basename(sys.argv[0])}
  269. parser = optparse.OptionParser(usage)
  270. parser.add_option('-c', '--color', metavar='on|off|auto', default='auto',
  271. help='enforce color' 'on|off|auto, default is auto')
  272. parser.add_option('-n', '--number', action='store_true',
  273. help='show line number')
  274. parser.add_option('-w', '--width', type='int', default=0,
  275. help='set text width for each side')
  276. parser.add_option('-t', '--traditional', action='store_true',
  277. help=('show in traditional format other than default side-by-side '
  278. 'mode (omit -n, -w)'))
  279. opts, args = parser.parse_args()
  280. if opts.color == 'yes':
  281. show_color = True
  282. elif opts.color == 'no':
  283. show_color = False
  284. elif opts.color == 'auto':
  285. show_color = sys.stdout.isatty()
  286. else:
  287. sys.stderr.write('Invalid color mode, try --help option for usage\n')
  288. sys.exit(1)
  289. if opts.width < 0:
  290. opts.width = 0
  291. if len(args) >= 1:
  292. diff_hdl = open(args[0], 'r')
  293. elif sys.stdin.isatty():
  294. sys.stderr.write('Try --help option for usage\n')
  295. sys.exit(1)
  296. else:
  297. diff_hdl = sys.stdin
  298. stream = diff_hdl.readlines()
  299. if diff_hdl is not sys.stdin:
  300. diff_hdl.close()
  301. diffviewer = DiffViewer(stream)
  302. view = diffviewer.view(show_color=show_color, show_number=opts.number,
  303. width=opts.width, traditional=opts.traditional)
  304. if sys.stdout.isatty():
  305. # args stolen fron git source, see less(1)
  306. # https://github.com/git/git/blob/master/pager.c
  307. pager = subprocess.Popen(['less', '-FRSXK'],
  308. stdin=subprocess.PIPE, stdout=sys.stdout)
  309. pager.stdin.write(''.join(view))
  310. pager.stdin.close()
  311. pager.wait()
  312. else:
  313. sys.stdout.write(''.join(view))
  314. sys.exit(0)
  315. # vim:set et sts=4 sw=4 tw=80: