123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. Term based tool to view *colored*, *incremental* diff in a *Git/Mercurial/Svn*
  5. workspace or from stdin, with *side by side* and *auto pager* support. Requires
  6. python (>= 2.5.0) and ``less``.
  7. """
  8. META_INFO = {
  9. 'version' : '0.8',
  10. 'license' : 'BSD-3',
  11. 'author' : 'Matthew Wang',
  12. 'email' : 'mattwyl(@)gmail(.)com',
  13. 'url' : 'https://github.com/ymattw/cdiff',
  14. 'keywords' : 'colored incremental side-by-side diff',
  15. 'description' : ('View colored, incremental diff in a workspace or from '
  16. 'stdin, with side by side and auto pager support')
  17. }
  18. import sys
  19. if sys.hexversion < 0x02050000:
  20. raise SystemExit("*** Requires python >= 2.5.0") # pragma: no cover
  21. import re
  22. import subprocess
  23. import errno
  24. import difflib
  25. COLORS = {
  26. 'reset' : '\x1b[0m',
  27. 'underline' : '\x1b[4m',
  28. 'reverse' : '\x1b[7m',
  29. 'red' : '\x1b[31m',
  30. 'green' : '\x1b[32m',
  31. 'yellow' : '\x1b[33m',
  32. 'blue' : '\x1b[34m',
  33. 'magenta' : '\x1b[35m',
  34. 'cyan' : '\x1b[36m',
  35. 'lightred' : '\x1b[1;31m',
  36. 'lightgreen' : '\x1b[1;32m',
  37. 'lightyellow' : '\x1b[1;33m',
  38. 'lightblue' : '\x1b[1;34m',
  39. 'lightmagenta' : '\x1b[1;35m',
  40. 'lightcyan' : '\x1b[1;36m',
  41. }
  42. # Keys for revision control probe, diff and log with diff
  43. VCS_INFO = {
  44. 'Git': {
  45. 'probe' : ['git', 'rev-parse'],
  46. 'diff' : ['git', 'diff'],
  47. 'log' : ['git', 'log', '--patch'],
  48. },
  49. 'Mercurial': {
  50. 'probe' : ['hg', 'summary'],
  51. 'diff' : ['hg', 'diff'],
  52. 'log' : ['hg', 'log', '--patch'],
  53. },
  54. 'Svn': {
  55. 'probe' : ['svn', 'info'],
  56. 'diff' : ['svn', 'diff'],
  57. 'log' : ['svn', 'log', '--diff', '--use-merge-history'],
  58. },
  59. }
  60. def colorize(text, start_color, end_color='reset'):
  61. return COLORS[start_color] + text + COLORS[end_color]
  62. class Hunk(object):
  63. def __init__(self, hunk_headers, hunk_meta, old_addr, new_addr):
  64. self._hunk_headers = hunk_headers
  65. self._hunk_meta = hunk_meta
  66. self._old_addr = old_addr # tuple (start, offset)
  67. self._new_addr = new_addr # tuple (start, offset)
  68. self._hunk_list = [] # list of tuple (attr, line)
  69. def append(self, hunk_line):
  70. """hunk_line is a 2-element tuple: (attr, text), where attr is:
  71. '-': old, '+': new, ' ': common
  72. """
  73. self._hunk_list.append(hunk_line)
  74. def mdiff(self):
  75. r"""The difflib._mdiff() function returns an interator which returns a
  76. tuple: (from line tuple, to line tuple, boolean flag)
  77. from/to line tuple -- (line num, line text)
  78. line num -- integer or None (to indicate a context separation)
  79. line text -- original line text with following markers inserted:
  80. '\0+' -- marks start of added text
  81. '\0-' -- marks start of deleted text
  82. '\0^' -- marks start of changed text
  83. '\1' -- marks end of added/deleted/changed text
  84. boolean flag -- None indicates context separation, True indicates
  85. either "from" or "to" line contains a change, otherwise False.
  86. """
  87. return difflib._mdiff(self._get_old_text(), self._get_new_text())
  88. def _get_old_text(self):
  89. out = []
  90. for (attr, line) in self._hunk_list:
  91. if attr != '+':
  92. out.append(line)
  93. return out
  94. def _get_new_text(self):
  95. out = []
  96. for (attr, line) in self._hunk_list:
  97. if attr != '-':
  98. out.append(line)
  99. return out
  100. class DiffOps(object): # pragma: no cover
  101. """Methods in this class are supposed to be overwritten by derived class.
  102. No is_header() anymore, all non-recognized lines are considered as headers
  103. """
  104. def is_old_path(self, line):
  105. return False
  106. def is_new_path(self, line):
  107. return False
  108. def is_hunk_meta(self, line):
  109. return False
  110. def parse_hunk_meta(self, line):
  111. """Returns a 2-element tuple, each is a tuple of (start, offset)"""
  112. return None
  113. def parse_hunk_line(self, line):
  114. """Returns a 2-element tuple: (attr, text), where attr is:
  115. '-': old, '+': new, ' ': common
  116. """
  117. return None
  118. def is_old(self, line):
  119. return False
  120. def is_new(self, line):
  121. return False
  122. def is_common(self, line):
  123. return False
  124. def is_eof(self, line):
  125. return False
  126. def is_only_in_dir(self, line):
  127. return False
  128. def is_binary_differ(self, line):
  129. return False
  130. class Diff(DiffOps):
  131. def __init__(self, headers, old_path, new_path, hunks):
  132. self._headers = headers
  133. self._old_path = old_path
  134. self._new_path = new_path
  135. self._hunks = hunks
  136. def markup_traditional(self):
  137. """Returns a generator"""
  138. for line in self._headers:
  139. yield self._markup_header(line)
  140. yield self._markup_old_path(self._old_path)
  141. yield self._markup_new_path(self._new_path)
  142. for hunk in self._hunks:
  143. for hunk_header in hunk._hunk_headers:
  144. yield self._markup_hunk_header(hunk_header)
  145. yield self._markup_hunk_meta(hunk._hunk_meta)
  146. for old, new, changed in hunk.mdiff():
  147. if changed:
  148. if not old[0]:
  149. # The '+' char after \x00 is kept
  150. # DEBUG: yield 'NEW: %s %s\n' % (old, new)
  151. line = new[1].strip('\x00\x01')
  152. yield self._markup_new(line)
  153. elif not new[0]:
  154. # The '-' char after \x00 is kept
  155. # DEBUG: yield 'OLD: %s %s\n' % (old, new)
  156. line = old[1].strip('\x00\x01')
  157. yield self._markup_old(line)
  158. else:
  159. # DEBUG: yield 'CHG: %s %s\n' % (old, new)
  160. yield self._markup_old('-') + \
  161. self._markup_mix(old[1], 'red')
  162. yield self._markup_new('+') + \
  163. self._markup_mix(new[1], 'green')
  164. else:
  165. yield self._markup_common(' ' + old[1])
  166. def markup_side_by_side(self, width):
  167. """Returns a generator"""
  168. wrap_char = colorize('>', 'lightmagenta')
  169. def _normalize(line):
  170. return line.replace(
  171. '\t', ' ' * 8).replace('\n', '').replace('\r', '')
  172. def _fit_with_marker(text, markup_fn, width, pad=False):
  173. """Wrap or pad input pure text, then markup"""
  174. if len(text) > width:
  175. return markup_fn(text[:(width - 1)]) + wrap_char
  176. elif pad:
  177. pad_len = width - len(text)
  178. return '%s%*s' % (markup_fn(text), pad_len, '')
  179. else:
  180. return markup_fn(text)
  181. def _fit_with_marker_mix(text, base_color, width, pad=False):
  182. """Wrap or pad input text which contains mdiff tags, markup at the
  183. meantime, note only left side need to set `pad`
  184. """
  185. out = [COLORS[base_color]]
  186. count = 0
  187. tag_re = re.compile(r'\x00[+^-]|\x01')
  188. while text and count < width:
  189. if text.startswith('\x00-'): # del
  190. out.append(COLORS['reverse'] + COLORS[base_color])
  191. text = text[2:]
  192. elif text.startswith('\x00+'): # add
  193. out.append(COLORS['reverse'] + COLORS[base_color])
  194. text = text[2:]
  195. elif text.startswith('\x00^'): # change
  196. out.append(COLORS['underline'] + COLORS[base_color])
  197. text = text[2:]
  198. elif text.startswith('\x01'): # reset
  199. out.append(COLORS['reset'] + COLORS[base_color])
  200. text = text[1:]
  201. else:
  202. # FIXME: utf-8 wchar might break the rule here, e.g.
  203. # u'\u554a' takes double width of a single letter, also
  204. # this depends on your terminal font. I guess audience of
  205. # this tool never put that kind of symbol in their code :-)
  206. #
  207. out.append(text[0])
  208. count += 1
  209. text = text[1:]
  210. if count == width and tag_re.sub('', text):
  211. # Was stripped: output fulfil and still has normal char in text
  212. out[-1] = COLORS['reset'] + wrap_char
  213. elif count < width and pad:
  214. pad_len = width - count
  215. out.append('%s%*s' % (COLORS['reset'], pad_len, ''))
  216. else:
  217. out.append(COLORS['reset'])
  218. return ''.join(out)
  219. # Set up line width
  220. if width <= 0:
  221. width = 80
  222. # Set up number width, note last hunk might be empty
  223. try:
  224. (start, offset) = self._hunks[-1]._old_addr
  225. max1 = start + offset - 1
  226. (start, offset) = self._hunks[-1]._new_addr
  227. max2 = start + offset - 1
  228. except IndexError:
  229. max1 = max2 = 0
  230. num_width = max(len(str(max1)), len(str(max2)))
  231. # Setup lineno and line format
  232. left_num_fmt = colorize('%%(left_num)%ds' % num_width, 'yellow')
  233. right_num_fmt = colorize('%%(right_num)%ds' % num_width, 'yellow')
  234. line_fmt = left_num_fmt + ' %(left)s ' + COLORS['reset'] + \
  235. right_num_fmt + ' %(right)s\n'
  236. # yield header, old path and new path
  237. for line in self._headers:
  238. yield self._markup_header(line)
  239. yield self._markup_old_path(self._old_path)
  240. yield self._markup_new_path(self._new_path)
  241. # yield hunks
  242. for hunk in self._hunks:
  243. for hunk_header in hunk._hunk_headers:
  244. yield self._markup_hunk_header(hunk_header)
  245. yield self._markup_hunk_meta(hunk._hunk_meta)
  246. for old, new, changed in hunk.mdiff():
  247. if old[0]:
  248. left_num = str(hunk._old_addr[0] + int(old[0]) - 1)
  249. else:
  250. left_num = ' '
  251. if new[0]:
  252. right_num = str(hunk._new_addr[0] + int(new[0]) - 1)
  253. else:
  254. right_num = ' '
  255. left = _normalize(old[1])
  256. right = _normalize(new[1])
  257. if changed:
  258. if not old[0]:
  259. left = '%*s' % (width, ' ')
  260. right = right.lstrip('\x00+').rstrip('\x01')
  261. right = _fit_with_marker(
  262. right, self._markup_new, width)
  263. elif not new[0]:
  264. left = left.lstrip('\x00-').rstrip('\x01')
  265. left = _fit_with_marker(left, self._markup_old, width)
  266. right = ''
  267. else:
  268. left = _fit_with_marker_mix(left, 'red', width, 1)
  269. right = _fit_with_marker_mix(right, 'green', width)
  270. else:
  271. left = _fit_with_marker(
  272. left, self._markup_common, width, 1)
  273. right = _fit_with_marker(right, self._markup_common, width)
  274. yield line_fmt % {
  275. 'left_num': left_num,
  276. 'left': left,
  277. 'right_num': right_num,
  278. 'right': right
  279. }
  280. def _markup_header(self, line):
  281. return colorize(line, 'cyan')
  282. def _markup_old_path(self, line):
  283. return colorize(line, 'yellow')
  284. def _markup_new_path(self, line):
  285. return colorize(line, 'yellow')
  286. def _markup_hunk_header(self, line):
  287. return colorize(line, 'lightcyan')
  288. def _markup_hunk_meta(self, line):
  289. return colorize(line, 'lightblue')
  290. def _markup_common(self, line):
  291. return colorize(line, 'reset')
  292. def _markup_old(self, line):
  293. return colorize(line, 'lightred')
  294. def _markup_new(self, line):
  295. return colorize(line, 'lightgreen')
  296. def _markup_mix(self, line, base_color):
  297. del_code = COLORS['reverse'] + COLORS[base_color]
  298. add_code = COLORS['reverse'] + COLORS[base_color]
  299. chg_code = COLORS['underline'] + COLORS[base_color]
  300. rst_code = COLORS['reset'] + COLORS[base_color]
  301. line = line.replace('\x00-', del_code)
  302. line = line.replace('\x00+', add_code)
  303. line = line.replace('\x00^', chg_code)
  304. line = line.replace('\x01', rst_code)
  305. return colorize(line, base_color)
  306. class UnifiedDiff(Diff):
  307. def is_old_path(self, line):
  308. return line.startswith('--- ')
  309. def is_new_path(self, line):
  310. return line.startswith('+++ ')
  311. def is_hunk_meta(self, line):
  312. """Minimal valid hunk meta is like '@@ -1 +1 @@', note extra chars
  313. might occur after the ending @@, e.g. in git log. '## ' uaually
  314. indicates svn property changes in output from `svn log --diff`
  315. """
  316. return (line.startswith('@@ -') and line.find(' @@') >= 8) or \
  317. (line.startswith('## -') and line.find(' ##') >= 8)
  318. def parse_hunk_meta(self, hunk_meta):
  319. # @@ -3,7 +3,6 @@
  320. a = hunk_meta.split()[1].split(',') # -3 7
  321. if len(a) > 1:
  322. old_addr = (int(a[0][1:]), int(a[1]))
  323. else:
  324. # @@ -1 +1,2 @@
  325. old_addr = (int(a[0][1:]), 0)
  326. b = hunk_meta.split()[2].split(',') # +3 6
  327. if len(b) > 1:
  328. new_addr = (int(b[0][1:]), int(b[1]))
  329. else:
  330. # @@ -0,0 +1 @@
  331. new_addr = (int(b[0][1:]), 0)
  332. return (old_addr, new_addr)
  333. def parse_hunk_line(self, line):
  334. return (line[0], line[1:])
  335. def is_old(self, line):
  336. """Exclude old path and header line from svn log --diff output, allow
  337. '----' likely to see in diff from yaml file
  338. """
  339. return line.startswith('-') and not self.is_old_path(line) and \
  340. not re.match(r'^-{72}$', line.rstrip())
  341. def is_new(self, line):
  342. return line.startswith('+') and not self.is_new_path(line)
  343. def is_common(self, line):
  344. return line.startswith(' ')
  345. def is_eof(self, line):
  346. # \ No newline at end of file
  347. # \ No newline at end of property
  348. return line.startswith(r'\ No newline at end of')
  349. def is_only_in_dir(self, line):
  350. return line.startswith('Only in ')
  351. def is_binary_differ(self, line):
  352. return re.match('^Binary files .* differ$', line.rstrip())
  353. class ContextDiff(Diff):
  354. pass
  355. class PatchStream(object):
  356. def __init__(self, diff_hdl):
  357. self._diff_hdl = diff_hdl
  358. self._stream_header_size = 0
  359. self._stream_header = []
  360. # Test whether stream is empty by read 1 line
  361. line = self._diff_hdl.readline()
  362. if not line:
  363. self._is_empty = True
  364. else:
  365. self._stream_header.append(line)
  366. self._stream_header_size += 1
  367. self._is_empty = False
  368. def is_empty(self):
  369. return self._is_empty
  370. def read_stream_header(self, stream_header_size):
  371. """Returns a small chunk for patch type detect, suppose to call once"""
  372. for i in range(1, stream_header_size):
  373. line = self._diff_hdl.readline()
  374. if not line:
  375. break
  376. self._stream_header.append(line)
  377. self._stream_header_size += 1
  378. return self._stream_header
  379. def __iter__(self):
  380. for line in self._stream_header:
  381. yield line
  382. for line in self._diff_hdl:
  383. yield line
  384. class DiffParser(object):
  385. def __init__(self, stream):
  386. self._stream = stream
  387. header = [decode(line) for line in
  388. self._stream.read_stream_header(100)]
  389. size = len(header)
  390. if size >= 4 and (header[0].startswith('*** ') and
  391. header[1].startswith('--- ') and
  392. header[2].rstrip() == '***************' and
  393. header[3].startswith('*** ') and
  394. header[3].rstrip().endswith(' ****')):
  395. self._type = 'context'
  396. return
  397. for n in range(size):
  398. if header[n].startswith('--- ') and (n < size - 1) and \
  399. header[n+1].startswith('+++ '):
  400. self._type = 'unified'
  401. break
  402. else:
  403. if size < 4:
  404. # It's safe to consider as udiff if patch stream contains no
  405. # more than 3 lines, happens with `git diff` on a file that
  406. # only has perm bits changes
  407. #
  408. self._type = 'unified'
  409. else:
  410. raise RuntimeError('unknown diff type')
  411. def get_diff_generator(self):
  412. """parse all diff lines, construct a list of Diff objects"""
  413. if self._type == 'unified':
  414. difflet = UnifiedDiff(None, None, None, None)
  415. else:
  416. raise RuntimeError('unsupported diff format')
  417. diff = Diff([], None, None, [])
  418. headers = []
  419. for line in self._stream:
  420. line = decode(line)
  421. if difflet.is_old_path(line):
  422. # FIXME: '--- ' breaks here, need to probe next 3 lines,
  423. # reproducible with github raw patch
  424. #
  425. if diff._old_path and diff._new_path and len(diff._hunks) > 0:
  426. # One diff constructed
  427. yield diff
  428. diff = Diff([], None, None, [])
  429. diff = Diff(headers, line, None, [])
  430. headers = []
  431. elif difflet.is_new_path(line):
  432. diff._new_path = line
  433. elif difflet.is_hunk_meta(line):
  434. hunk_meta = line
  435. try:
  436. old_addr, new_addr = difflet.parse_hunk_meta(hunk_meta)
  437. except (IndexError, ValueError):
  438. raise RuntimeError('invalid hunk meta: %s' % hunk_meta)
  439. hunk = Hunk(headers, hunk_meta, old_addr, new_addr)
  440. headers = []
  441. diff._hunks.append(hunk)
  442. elif len(diff._hunks) > 0 and (difflet.is_old(line) or
  443. difflet.is_new(line) or
  444. difflet.is_common(line)):
  445. diff._hunks[-1].append(difflet.parse_hunk_line(line))
  446. elif difflet.is_eof(line):
  447. # ignore
  448. pass
  449. elif difflet.is_only_in_dir(line) or \
  450. difflet.is_binary_differ(line):
  451. # 'Only in foo:' and 'Binary files ... differ' are considered
  452. # as separate diffs, so yield current diff, then this line
  453. #
  454. if diff._old_path and diff._new_path and len(diff._hunks) > 0:
  455. # Current diff is comppletely constructed
  456. yield diff
  457. headers.append(line)
  458. yield Diff(headers, '', '', [])
  459. headers = []
  460. diff = Diff([], None, None, [])
  461. else:
  462. # All other non-recognized lines are considered as headers or
  463. # hunk headers respectively
  464. #
  465. headers.append(line)
  466. # Validate and yield the last patch set if it is not yielded yet
  467. if diff._old_path:
  468. assert diff._new_path is not None
  469. if diff._hunks:
  470. assert len(diff._hunks[-1]._hunk_meta) > 0
  471. assert len(diff._hunks[-1]._hunk_list) > 0
  472. yield diff
  473. if headers:
  474. # Tolerate dangling headers, just yield a Diff object with only
  475. # header lines
  476. #
  477. yield Diff(headers, '', '', [])
  478. class DiffMarkup(object):
  479. def __init__(self, stream):
  480. self._diffs = DiffParser(stream).get_diff_generator()
  481. def markup(self, side_by_side=False, width=0):
  482. """Returns a generator"""
  483. if side_by_side:
  484. return self._markup_side_by_side(width)
  485. else:
  486. return self._markup_traditional()
  487. def _markup_traditional(self):
  488. for diff in self._diffs:
  489. for line in diff.markup_traditional():
  490. yield line
  491. def _markup_side_by_side(self, width):
  492. for diff in self._diffs:
  493. for line in diff.markup_side_by_side(width):
  494. yield line
  495. def markup_to_pager(stream, opts):
  496. markup = DiffMarkup(stream)
  497. color_diff = markup.markup(side_by_side=opts.side_by_side,
  498. width=opts.width)
  499. # Args stolen from git source: github.com/git/git/blob/master/pager.c
  500. pager = subprocess.Popen(
  501. ['less', '-FRSX'], stdin=subprocess.PIPE, stdout=sys.stdout)
  502. try:
  503. for line in color_diff:
  504. pager.stdin.write(line.encode('utf-8'))
  505. except KeyboardInterrupt:
  506. pass
  507. pager.stdin.close()
  508. pager.wait()
  509. def check_command_status(arguments):
  510. """Return True if command returns 0."""
  511. try:
  512. return subprocess.call(
  513. arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0
  514. except OSError:
  515. return False
  516. def revision_control_diff(args):
  517. """Return diff from revision control system."""
  518. for _, ops in VCS_INFO.items():
  519. if check_command_status(ops['probe']):
  520. return subprocess.Popen(
  521. ops['diff'] + args, stdout=subprocess.PIPE).stdout
  522. def revision_control_log(args):
  523. """Return log from revision control system."""
  524. for _, ops in VCS_INFO.items():
  525. if check_command_status(ops['probe']):
  526. return subprocess.Popen(
  527. ops['log'] + args, stdout=subprocess.PIPE).stdout
  528. def decode(line):
  529. """Decode UTF-8 if necessary."""
  530. try:
  531. return line.decode('utf-8')
  532. except AttributeError:
  533. return line
  534. def main():
  535. import optparse
  536. supported_vcs = sorted(VCS_INFO.keys())
  537. usage = """%prog [options] [file|dir ...]"""
  538. parser = optparse.OptionParser(
  539. usage=usage, description=META_INFO['description'],
  540. version='%%prog %s' % META_INFO['version'])
  541. parser.add_option(
  542. '-s', '--side-by-side', action='store_true',
  543. help='enable side-by-side mode')
  544. parser.add_option(
  545. '-w', '--width', type='int', default=80, metavar='N',
  546. help='set text width for side-by-side mode, default is 80')
  547. parser.add_option(
  548. '-l', '--log', action='store_true',
  549. help='show log with changes from revision control')
  550. parser.add_option(
  551. '-c', '--color', default='auto', metavar='X',
  552. help="""colorize mode 'auto' (default), 'always', or 'never'""")
  553. opts, args = parser.parse_args()
  554. if opts.log:
  555. diff_hdl = revision_control_log(args)
  556. if not diff_hdl:
  557. sys.stderr.write(('*** Not in a supported workspace, supported '
  558. 'are: %s\n') % ', '.join(supported_vcs))
  559. return 1
  560. elif sys.stdin.isatty():
  561. diff_hdl = revision_control_diff(args)
  562. if not diff_hdl:
  563. sys.stderr.write(('*** Not in a supported workspace, supported '
  564. 'are: %s\n\n') % ', '.join(supported_vcs))
  565. parser.print_help()
  566. return 1
  567. else:
  568. diff_hdl = sys.stdin
  569. stream = PatchStream(diff_hdl)
  570. # Don't let empty diff pass thru
  571. if stream.is_empty():
  572. return 0
  573. if opts.color == 'always' or \
  574. (opts.color == 'auto' and sys.stdout.isatty()):
  575. try:
  576. markup_to_pager(stream, opts)
  577. except IOError:
  578. e = sys.exc_info()[1]
  579. if e.errno == errno.EPIPE:
  580. pass
  581. else:
  582. # pipe out stream untouched to make sure it is still a patch
  583. for line in stream:
  584. sys.stdout.write(decode(line))
  585. if diff_hdl is not sys.stdin:
  586. diff_hdl.close()
  587. return 0
  588. if __name__ == '__main__':
  589. sys.exit(main())
  590. # vim:set et sts=4 sw=4 tw=79: