cdiff.py 26KB

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