Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100755 787 lines (653 sloc) 28.13 kb
b02e23bb »
2013-01-29 initial revision
1 #!/usr/bin/env python
59e21fcf »
2013-01-31 better _fit_width(); FIXME: out[-1] might broken, see diff of this co…
2 # -*- coding: utf-8 -*-
b02e23bb »
2013-01-29 initial revision
3
f6f67814 »
2013-02-01 gitignore and doc update
4 """
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
5 Term based tool to view *colored*, *incremental* diff in a *Git/Mercurial/Svn*
6 workspace or from stdin, with *side by side* and *auto pager* support. Requires
7 python (>= 2.5.0) and ``less``.
f6f67814 »
2013-02-01 gitignore and doc update
8 """
9
4bcbeff4 »
2013-02-06 Fix issue #8 version awfullness
10 META_INFO = {
121c28bc »
2014-06-20 Version 0.9.6 Fix TypeError exception in auto width logic
11 'version' : '0.9.6',
4bcbeff4 »
2013-02-06 Fix issue #8 version awfullness
12 'license' : 'BSD-3',
13 'author' : 'Matthew Wang',
14 'email' : 'mattwyl(@)gmail(.)com',
15 'url' : 'https://github.com/ymattw/cdiff',
16 'keywords' : 'colored incremental side-by-side diff',
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
17 'description' : ('View colored, incremental diff in a workspace or from '
176bd406 »
2013-02-23 Document/usage update for 0.7
18 'stdin, with side by side and auto pager support')
4bcbeff4 »
2013-02-06 Fix issue #8 version awfullness
19 }
20
b02e23bb »
2013-01-29 initial revision
21 import sys
c8734e4e »
2013-02-01 refuse to run with python < 2.5.0
22
23 if sys.hexversion < 0x02050000:
b601b620 »
2013-02-26 Use '# pragma: no cover' to ignore 1 line logic
24 raise SystemExit("*** Requires python >= 2.5.0") # pragma: no cover
c8734e4e »
2013-02-01 refuse to run with python < 2.5.0
25
12efc9d2 »
2013-09-27 Implement next() for Python 2.5
26 # Python < 2.6 does not have next()
27 try:
28 next
29 except NameError:
30 def next(obj): return obj.next()
31
db3bc92b » rakuco
2013-12-10 Respect the `LESS' environment variable.
32 import os
b02e23bb »
2013-01-29 initial revision
33 import re
2cb541c9 » myint
2013-08-23 Handle all keyboard interrupts more completely
34 import signal
ed0ce1cb » myint
2013-02-02 Enable use as a revision control diff
35 import subprocess
d4ed688e »
2013-09-10 Another try for issue #30
36 import select
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
37 import difflib
38
39
09574025 » myint
2013-10-12 Handle Latin-1 encoded text in diffs
40 try:
41 unicode
42 except NameError:
43 unicode = str
44
45
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
46 COLORS = {
47 'reset' : '\x1b[0m',
0c672b13 »
2013-01-31 side-by-side now fit width if len(line) < 80; FIXME: reset term after…
48 'underline' : '\x1b[4m',
49 'reverse' : '\x1b[7m',
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
50 'red' : '\x1b[31m',
51 'green' : '\x1b[32m',
52 'yellow' : '\x1b[33m',
53 'blue' : '\x1b[34m',
54 'magenta' : '\x1b[35m',
55 'cyan' : '\x1b[36m',
56 'lightred' : '\x1b[1;31m',
57 'lightgreen' : '\x1b[1;32m',
58 'lightyellow' : '\x1b[1;33m',
51dbcfb0 »
2013-01-30 more flexible mix color; discard hunk addr stuff
59 'lightblue' : '\x1b[1;34m',
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
60 'lightmagenta' : '\x1b[1;35m',
61 'lightcyan' : '\x1b[1;36m',
62 }
63
41c1e5bc » myint
2013-02-02 Factor out common code
64
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
65 # Keys for revision control probe, diff and log with diff
66 VCS_INFO = {
67 'Git': {
68 'probe' : ['git', 'rev-parse'],
e19f384b »
2013-05-20 Fix issue #23 with --no-ext-diff
69 'diff' : ['git', 'diff', '--no-ext-diff'],
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
70 'log' : ['git', 'log', '--patch'],
71 },
72 'Mercurial': {
73 'probe' : ['hg', 'summary'],
74 'diff' : ['hg', 'diff'],
75 'log' : ['hg', 'log', '--patch'],
76 },
77 'Svn': {
78 'probe' : ['svn', 'info'],
79 'diff' : ['svn', 'diff'],
ec6d34da » myint
2013-02-19 Show merge history in Subversion log
80 'log' : ['svn', 'log', '--diff', '--use-merge-history'],
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
81 },
82 }
41c1e5bc » myint
2013-02-02 Factor out common code
83
84
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
85 def colorize(text, start_color, end_color='reset'):
58bb01b0 »
2013-02-19 Performance enhancement
86 return COLORS[start_color] + text + COLORS[end_color]
b02e23bb »
2013-01-29 initial revision
87
88
89 class Hunk(object):
90
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
91 def __init__(self, hunk_headers, hunk_meta, old_addr, new_addr):
92 self._hunk_headers = hunk_headers
93 self._hunk_meta = hunk_meta
ebbba926 »
2013-01-31 support line number display; remove -n option
94 self._old_addr = old_addr # tuple (start, offset)
1126142d »
2013-02-02 Fix issue #3 Broken at strange hunk head in a valid git patch
95 self._new_addr = new_addr # tuple (start, offset)
ebbba926 »
2013-01-31 support line number display; remove -n option
96 self._hunk_list = [] # list of tuple (attr, line)
b02e23bb »
2013-01-29 initial revision
97
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
98 def append(self, hunk_line):
e4eccf3f »
2013-02-21 Code and doc lint
99 """hunk_line is a 2-element tuple: (attr, text), where attr is:
100 '-': old, '+': new, ' ': common
101 """
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
102 self._hunk_list.append(hunk_line)
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
103
104 def mdiff(self):
e1ce6395 » myint
2013-02-02 Add "r" literal to docstring
105 r"""The difflib._mdiff() function returns an interator which returns a
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
106 tuple: (from line tuple, to line tuple, boolean flag)
107
108 from/to line tuple -- (line num, line text)
109 line num -- integer or None (to indicate a context separation)
110 line text -- original line text with following markers inserted:
111 '\0+' -- marks start of added text
112 '\0-' -- marks start of deleted text
113 '\0^' -- marks start of changed text
114 '\1' -- marks end of added/deleted/changed text
115
116 boolean flag -- None indicates context separation, True indicates
117 either "from" or "to" line contains a change, otherwise False.
118 """
119 return difflib._mdiff(self._get_old_text(), self._get_new_text())
120
121 def _get_old_text(self):
122 out = []
123 for (attr, line) in self._hunk_list:
124 if attr != '+':
125 out.append(line)
126 return out
127
128 def _get_new_text(self):
129 out = []
130 for (attr, line) in self._hunk_list:
131 if attr != '-':
132 out.append(line)
133 return out
b02e23bb »
2013-01-29 initial revision
134
135
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
136 class UnifiedDiff(object):
45b95d6e »
2013-02-26 Split Diff detector methods to class DiffOps
137
138 def __init__(self, headers, old_path, new_path, hunks):
139 self._headers = headers
140 self._old_path = old_path
141 self._new_path = new_path
142 self._hunks = hunks
a413a035 »
2013-02-23 Handle 'Binary files ... differ'
143
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
144 def is_old_path(self, line):
b02e23bb »
2013-01-29 initial revision
145 return line.startswith('--- ')
146
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
147 def is_new_path(self, line):
b02e23bb »
2013-01-29 initial revision
148 return line.startswith('+++ ')
149
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
150 def is_hunk_meta(self, line):
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
151 """Minimal valid hunk meta is like '@@ -1 +1 @@', note extra chars
2e49a7eb »
2013-03-20 Fall through unknown format to 'unified', fixed #18
152 might occur after the ending @@, e.g. in git log. '## ' usually
7881a41f »
2013-03-16 - Naming enhancement for unified diff
153 indicates svn property changes in output from `svn log --diff`
6e60a56c »
2013-02-20 Make patch parser more solid
154 """
155 return (line.startswith('@@ -') and line.find(' @@') >= 8) or \
156 (line.startswith('## -') and line.find(' ##') >= 8)
b02e23bb »
2013-01-29 initial revision
157
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
158 def parse_hunk_meta(self, hunk_meta):
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
159 # @@ -3,7 +3,6 @@
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
160 a = hunk_meta.split()[1].split(',') # -3 7
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
161 if len(a) > 1:
162 old_addr = (int(a[0][1:]), int(a[1]))
163 else:
164 # @@ -1 +1,2 @@
165 old_addr = (int(a[0][1:]), 0)
166
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
167 b = hunk_meta.split()[2].split(',') # +3 6
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
168 if len(b) > 1:
169 new_addr = (int(b[0][1:]), int(b[1]))
170 else:
171 # @@ -0,0 +1 @@
172 new_addr = (int(b[0][1:]), 0)
b02e23bb »
2013-01-29 initial revision
173
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
174 return (old_addr, new_addr)
b02e23bb »
2013-01-29 initial revision
175
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
176 def parse_hunk_line(self, line):
177 return (line[0], line[1:])
178
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
179 def is_old(self, line):
6e60a56c »
2013-02-20 Make patch parser more solid
180 """Exclude old path and header line from svn log --diff output, allow
181 '----' likely to see in diff from yaml file
182 """
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
183 return line.startswith('-') and not self.is_old_path(line) and \
6a6249f4 »
2013-03-10 Dirty fix false alarm of dangling header reported in issue #14
184 not re.match(r'^-{72}$', line.rstrip())
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
185
186 def is_new(self, line):
187 return line.startswith('+') and not self.is_new_path(line)
188
189 def is_common(self, line):
b02e23bb »
2013-01-29 initial revision
190 return line.startswith(' ')
191
b9f55076 »
2013-02-05 document/usage update; refactor for better support other diff formats…
192 def is_eof(self, line):
b02e23bb »
2013-01-29 initial revision
193 # \ No newline at end of file
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
194 # \ No newline at end of property
195 return line.startswith(r'\ No newline at end of')
b02e23bb »
2013-01-29 initial revision
196
0525f7c1 »
2013-02-22 Handle 'Only in <dir>: ' header in output from diff -ru d1 d2
197 def is_only_in_dir(self, line):
198 return line.startswith('Only in ')
199
a413a035 »
2013-02-23 Handle 'Binary files ... differ'
200 def is_binary_differ(self, line):
201 return re.match('^Binary files .* differ$', line.rstrip())
202
b02e23bb »
2013-01-29 initial revision
203
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
204 class PatchStream(object):
205
206 def __init__(self, diff_hdl):
207 self._diff_hdl = diff_hdl
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
208 self._stream_header_size = 0
209 self._stream_header = []
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
210
211 # Test whether stream is empty by read 1 line
212 line = self._diff_hdl.readline()
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
213 if not line:
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
214 self._is_empty = True
215 else:
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
216 self._stream_header.append(line)
217 self._stream_header_size += 1
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
218 self._is_empty = False
219
220 def is_empty(self):
221 return self._is_empty
222
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
223 def read_stream_header(self, stream_header_size):
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
224 """Returns a small chunk for patch type detect, suppose to call once"""
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
225 for i in range(1, stream_header_size):
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
226 line = self._diff_hdl.readline()
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
227 if not line:
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
228 break
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
229 self._stream_header.append(line)
230 self._stream_header_size += 1
231 return self._stream_header
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
232
233 def __iter__(self):
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
234 for line in self._stream_header:
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
235 yield line
236 for line in self._diff_hdl:
237 yield line
238
239
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
240 class PatchStreamForwarder(object):
d4ed688e »
2013-09-10 Another try for issue #30
241 """A blocking stream forwarder use `select` and line buffered mode. Feed
242 input stream to a diff format translator and read output stream from it.
243 Note input stream is non-seekable, and upstream has eaten some lines.
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
244 """
245 def __init__(self, istream, translator):
246 assert isinstance(istream, PatchStream)
d4ed688e »
2013-09-10 Another try for issue #30
247 assert isinstance(translator, subprocess.Popen)
248 self._istream = iter(istream)
249 self._in = translator.stdin
250 self._out = translator.stdout
251
252 def _can_read(self, timeout=0):
253 return select.select([self._out.fileno()], [], [], timeout)[0]
254
255 def _forward_line(self):
256 try:
257 line = next(self._istream)
09574025 » myint
2013-10-12 Handle Latin-1 encoded text in diffs
258 self._in.write(line)
d4ed688e »
2013-09-10 Another try for issue #30
259 except StopIteration:
260 self._in.close()
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
261
262 def __iter__(self):
263 while True:
d4ed688e »
2013-09-10 Another try for issue #30
264 if self._can_read():
265 line = self._out.readline()
266 if line:
267 yield line
268 else:
269 return
270 elif not self._in.closed:
271 self._forward_line()
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
272
273
b02e23bb »
2013-01-29 initial revision
274 class DiffParser(object):
275
276 def __init__(self, stream):
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
277
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
278 header = [decode(line) for line in stream.read_stream_header(100)]
e191e54e »
2013-03-13 Fixed #14 by tolerating dangling headers and short patch < 4 lines
279 size = len(header)
7881a41f »
2013-03-16 - Naming enhancement for unified diff
280
69e4b168 »
2013-03-16 Probe context diff for #15
281 if size >= 4 and (header[0].startswith('*** ') and
282 header[1].startswith('--- ') and
b87376fa »
2013-03-19 Should tolerate dos format
283 header[2].rstrip() == '***************' and
69e4b168 »
2013-03-16 Probe context diff for #15
284 header[3].startswith('*** ') and
b87376fa »
2013-03-19 Should tolerate dos format
285 header[3].rstrip().endswith(' ****')):
2e49a7eb »
2013-03-20 Fall through unknown format to 'unified', fixed #18
286 # For context diff, try use `filterdiff` to translate it to unified
287 # format and provide a new stream
288 #
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
289 self._type = 'context'
290 try:
d4ed688e »
2013-09-10 Another try for issue #30
291 # Use line buffered mode so that to readline() in block mode
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
292 self._translator = subprocess.Popen(
293 ['filterdiff', '--format=unified'], stdin=subprocess.PIPE,
d4ed688e »
2013-09-10 Another try for issue #30
294 stdout=subprocess.PIPE, bufsize=1)
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
295 except OSError:
296 raise SystemExit('*** Context diff support depends on '
297 'filterdiff')
298 self._stream = PatchStreamForwarder(stream, self._translator)
69e4b168 »
2013-03-16 Probe context diff for #15
299 return
300
e191e54e »
2013-03-13 Fixed #14 by tolerating dangling headers and short patch < 4 lines
301 for n in range(size):
7881a41f »
2013-03-16 - Naming enhancement for unified diff
302 if header[n].startswith('--- ') and (n < size - 1) and \
303 header[n+1].startswith('+++ '):
304 self._type = 'unified'
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
305 self._stream = stream
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
306 break
b02e23bb »
2013-01-29 initial revision
307 else:
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
308 # `filterdiff` translates unknown diff to nothing, fall through to
2e49a7eb »
2013-03-20 Fall through unknown format to 'unified', fixed #18
309 # unified diff give cdiff a chance to show everything as headers
310 #
311 sys.stderr.write("*** unknown format, fall through to 'unified'\n")
312 self._type = 'unified'
e7854ddd »
2013-03-21 Support context diff via filterdiff, fixed #15
313 self._stream = stream
b02e23bb »
2013-01-29 initial revision
314
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
315 def get_diff_generator(self):
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
316 """parse all diff lines, construct a list of UnifiedDiff objects"""
317 diff = UnifiedDiff([], None, None, [])
b02e23bb »
2013-01-29 initial revision
318 headers = []
319
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
320 for line in self._stream:
321 line = decode(line)
322
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
323 if diff.is_old_path(line):
ab9a9981 »
2013-03-23 Enhance diff parser on handling hunk links, fix #20
324 # FIXME: '--- ' breaks here, better to probe next line
325 if diff._old_path and diff._new_path and diff._hunks:
326 # See a new diff, yield previous diff if exists
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
327 yield diff
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
328 diff = UnifiedDiff(headers, line, None, [])
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
329 headers = []
b02e23bb »
2013-01-29 initial revision
330
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
331 elif diff.is_new_path(line) and diff._old_path:
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
332 diff._new_path = line
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
333
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
334 elif diff.is_hunk_meta(line):
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
335 hunk_meta = line
a09947c1 »
2013-02-26 Enough unit tests (95%)
336 try:
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
337 old_addr, new_addr = diff.parse_hunk_meta(hunk_meta)
a09947c1 »
2013-02-26 Enough unit tests (95%)
338 except (IndexError, ValueError):
339 raise RuntimeError('invalid hunk meta: %s' % hunk_meta)
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
340 hunk = Hunk(headers, hunk_meta, old_addr, new_addr)
341 headers = []
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
342 diff._hunks.append(hunk)
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
343
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
344 elif diff._hunks and not headers and (diff.is_old(line) or
345 diff.is_new(line) or
346 diff.is_common(line)):
347 diff._hunks[-1].append(diff.parse_hunk_line(line))
b02e23bb »
2013-01-29 initial revision
348
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
349 elif diff.is_eof(line):
b02e23bb »
2013-01-29 initial revision
350 # ignore
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
351 pass
b02e23bb »
2013-01-29 initial revision
352
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
353 elif diff.is_only_in_dir(line) or \
354 diff.is_binary_differ(line):
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
355 # 'Only in foo:' and 'Binary files ... differ' are considered
356 # as separate diffs, so yield current diff, then this line
0525f7c1 »
2013-02-22 Handle 'Only in <dir>: ' header in output from diff -ru d1 d2
357 #
ab9a9981 »
2013-03-23 Enhance diff parser on handling hunk links, fix #20
358 if diff._old_path and diff._new_path and diff._hunks:
a413a035 »
2013-02-23 Handle 'Binary files ... differ'
359 # Current diff is comppletely constructed
0525f7c1 »
2013-02-22 Handle 'Only in <dir>: ' header in output from diff -ru d1 d2
360 yield diff
a413a035 »
2013-02-23 Handle 'Binary files ... differ'
361 headers.append(line)
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
362 yield UnifiedDiff(headers, '', '', [])
a413a035 »
2013-02-23 Handle 'Binary files ... differ'
363 headers = []
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
364 diff = UnifiedDiff([], None, None, [])
0525f7c1 »
2013-02-22 Handle 'Only in <dir>: ' header in output from diff -ru d1 d2
365
b02e23bb »
2013-01-29 initial revision
366 else:
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
367 # All other non-recognized lines are considered as headers or
368 # hunk headers respectively
369 #
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
370 headers.append(line)
20a8acac »
2013-02-18 Better patch parser; support svn log --diff
371
0525f7c1 »
2013-02-22 Handle 'Only in <dir>: ' header in output from diff -ru d1 d2
372 # Validate and yield the last patch set if it is not yielded yet
373 if diff._old_path:
374 assert diff._new_path is not None
375 if diff._hunks:
376 assert len(diff._hunks[-1]._hunk_meta) > 0
377 assert len(diff._hunks[-1]._hunk_list) > 0
378 yield diff
b02e23bb »
2013-01-29 initial revision
379
e191e54e »
2013-03-13 Fixed #14 by tolerating dangling headers and short patch < 4 lines
380 if headers:
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
381 # Tolerate dangling headers, just yield a UnifiedDiff object with
382 # only header lines
e191e54e »
2013-03-13 Fixed #14 by tolerating dangling headers and short patch < 4 lines
383 #
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
384 yield UnifiedDiff(headers, '', '', [])
e191e54e »
2013-03-13 Fixed #14 by tolerating dangling headers and short patch < 4 lines
385
b02e23bb »
2013-01-29 initial revision
386
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
387 class DiffMarker(object):
b02e23bb »
2013-01-29 initial revision
388
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
389 def markup(self, diffs, side_by_side=False, width=0):
0c672b13 »
2013-01-31 side-by-side now fit width if len(line) < 80; FIXME: reset term after…
390 """Returns a generator"""
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
391 if side_by_side:
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
392 for diff in diffs:
393 for line in self._markup_side_by_side(diff, width):
394 yield line
b02e23bb »
2013-01-29 initial revision
395 else:
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
396 for diff in diffs:
397 for line in self._markup_traditional(diff):
398 yield line
b02e23bb »
2013-01-29 initial revision
399
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
400 def _markup_traditional(self, diff):
401 """Returns a generator"""
402 for line in diff._headers:
403 yield self._markup_header(line)
b02e23bb »
2013-01-29 initial revision
404
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
405 yield self._markup_old_path(diff._old_path)
406 yield self._markup_new_path(diff._new_path)
407
408 for hunk in diff._hunks:
409 for hunk_header in hunk._hunk_headers:
410 yield self._markup_hunk_header(hunk_header)
411 yield self._markup_hunk_meta(hunk._hunk_meta)
412 for old, new, changed in hunk.mdiff():
413 if changed:
414 if not old[0]:
415 # The '+' char after \x00 is kept
416 # DEBUG: yield 'NEW: %s %s\n' % (old, new)
417 line = new[1].strip('\x00\x01')
418 yield self._markup_new(line)
419 elif not new[0]:
420 # The '-' char after \x00 is kept
421 # DEBUG: yield 'OLD: %s %s\n' % (old, new)
422 line = old[1].strip('\x00\x01')
423 yield self._markup_old(line)
424 else:
425 # DEBUG: yield 'CHG: %s %s\n' % (old, new)
426 yield self._markup_old('-') + \
427 self._markup_mix(old[1], 'red')
428 yield self._markup_new('+') + \
429 self._markup_mix(new[1], 'green')
430 else:
431 yield self._markup_common(' ' + old[1])
432
433 def _markup_side_by_side(self, diff, width):
434 """Returns a generator"""
435 wrap_char = colorize('>', 'lightmagenta')
436
437 def _normalize(line):
438 return line.replace(
439 '\t', ' ' * 8).replace('\n', '').replace('\r', '')
440
441 def _fit_with_marker(text, markup_fn, width, pad=False):
442 """Wrap or pad input pure text, then markup"""
443 if len(text) > width:
444 return markup_fn(text[:(width - 1)]) + wrap_char
445 elif pad:
446 pad_len = width - len(text)
447 return '%s%*s' % (markup_fn(text), pad_len, '')
448 else:
449 return markup_fn(text)
450
451 def _fit_with_marker_mix(text, base_color, width, pad=False):
452 """Wrap or pad input text which contains mdiff tags, markup at the
453 meantime, note only left side need to set `pad`
454 """
455 out = [COLORS[base_color]]
456 count = 0
457 tag_re = re.compile(r'\x00[+^-]|\x01')
458
459 while text and count < width:
460 if text.startswith('\x00-'): # del
461 out.append(COLORS['reverse'] + COLORS[base_color])
462 text = text[2:]
463 elif text.startswith('\x00+'): # add
464 out.append(COLORS['reverse'] + COLORS[base_color])
465 text = text[2:]
466 elif text.startswith('\x00^'): # change
467 out.append(COLORS['underline'] + COLORS[base_color])
468 text = text[2:]
469 elif text.startswith('\x01'): # reset
470 out.append(COLORS['reset'] + COLORS[base_color])
471 text = text[1:]
472 else:
473 # FIXME: utf-8 wchar might break the rule here, e.g.
474 # u'\u554a' takes double width of a single letter, also
475 # this depends on your terminal font. I guess audience of
476 # this tool never put that kind of symbol in their code :-)
477 #
478 out.append(text[0])
479 count += 1
480 text = text[1:]
481
482 if count == width and tag_re.sub('', text):
483 # Was stripped: output fulfil and still has normal char in text
484 out[-1] = COLORS['reset'] + wrap_char
485 elif count < width and pad:
486 pad_len = width - count
487 out.append('%s%*s' % (COLORS['reset'], pad_len, ''))
488 else:
489 out.append(COLORS['reset'])
490
491 return ''.join(out)
492
493 # Set up number width, note last hunk might be empty
494 try:
495 (start, offset) = diff._hunks[-1]._old_addr
496 max1 = start + offset - 1
497 (start, offset) = diff._hunks[-1]._new_addr
498 max2 = start + offset - 1
499 except IndexError:
500 max1 = max2 = 0
501 num_width = max(len(str(max1)), len(str(max2)))
01af713f »
2014-06-17 Auto setup line width once num_width is known
502
503 # Set up line width
504 if width <= 0:
505 # Autodetection of text width according to terminal size
506 try:
507 # Each line is like "nnn TEXT nnn TEXT\n", so width is half of
508 # [terminal size minus the line number columns and 3 separating
509 # spaces
510 #
18b3b85b » myint
2014-06-20 Use correct integer division
511 width = (terminal_size()[0] - num_width * 2 - 3) // 2
01af713f »
2014-06-17 Auto setup line width once num_width is known
512 except Exception:
513 # If terminal detection failed, set back to default
514 width = 80
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
515
516 # Setup lineno and line format
517 left_num_fmt = colorize('%%(left_num)%ds' % num_width, 'yellow')
518 right_num_fmt = colorize('%%(right_num)%ds' % num_width, 'yellow')
519 line_fmt = left_num_fmt + ' %(left)s ' + COLORS['reset'] + \
520 right_num_fmt + ' %(right)s\n'
521
522 # yield header, old path and new path
523 for line in diff._headers:
524 yield self._markup_header(line)
525 yield self._markup_old_path(diff._old_path)
526 yield self._markup_new_path(diff._new_path)
527
528 # yield hunks
529 for hunk in diff._hunks:
530 for hunk_header in hunk._hunk_headers:
531 yield self._markup_hunk_header(hunk_header)
532 yield self._markup_hunk_meta(hunk._hunk_meta)
533 for old, new, changed in hunk.mdiff():
534 if old[0]:
535 left_num = str(hunk._old_addr[0] + int(old[0]) - 1)
536 else:
537 left_num = ' '
538
539 if new[0]:
540 right_num = str(hunk._new_addr[0] + int(new[0]) - 1)
541 else:
542 right_num = ' '
543
544 left = _normalize(old[1])
545 right = _normalize(new[1])
546
547 if changed:
548 if not old[0]:
549 left = '%*s' % (width, ' ')
550 right = right.lstrip('\x00+').rstrip('\x01')
551 right = _fit_with_marker(
552 right, self._markup_new, width)
553 elif not new[0]:
554 left = left.lstrip('\x00-').rstrip('\x01')
555 left = _fit_with_marker(left, self._markup_old, width)
556 right = ''
557 else:
558 left = _fit_with_marker_mix(left, 'red', width, 1)
559 right = _fit_with_marker_mix(right, 'green', width)
560 else:
561 left = _fit_with_marker(
562 left, self._markup_common, width, 1)
563 right = _fit_with_marker(right, self._markup_common, width)
564 yield line_fmt % {
565 'left_num': left_num,
566 'left': left,
567 'right_num': right_num,
568 'right': right
569 }
570
571 def _markup_header(self, line):
572 return colorize(line, 'cyan')
573
574 def _markup_old_path(self, line):
575 return colorize(line, 'yellow')
576
577 def _markup_new_path(self, line):
578 return colorize(line, 'yellow')
579
580 def _markup_hunk_header(self, line):
581 return colorize(line, 'lightcyan')
582
583 def _markup_hunk_meta(self, line):
584 return colorize(line, 'lightblue')
585
586 def _markup_common(self, line):
587 return colorize(line, 'reset')
588
589 def _markup_old(self, line):
590 return colorize(line, 'lightred')
591
592 def _markup_new(self, line):
9d7c7231 »
2014-07-10 No more lightgreen, it's unreadable in solarized color scheme
593 return colorize(line, 'green')
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
594
595 def _markup_mix(self, line, base_color):
596 del_code = COLORS['reverse'] + COLORS[base_color]
597 add_code = COLORS['reverse'] + COLORS[base_color]
598 chg_code = COLORS['underline'] + COLORS[base_color]
599 rst_code = COLORS['reset'] + COLORS[base_color]
600 line = line.replace('\x00-', del_code)
601 line = line.replace('\x00+', add_code)
602 line = line.replace('\x00^', chg_code)
603 line = line.replace('\x01', rst_code)
604 return colorize(line, base_color)
b02e23bb »
2013-01-29 initial revision
605
606
3aa7040b » myint
2013-02-03 Add missing main()
607 def markup_to_pager(stream, opts):
17559700 »
2013-09-27 Adjust Popen object creation order to fix issue #30
608 """Pipe unified diff stream to pager (less).
609
610 Note: have to create pager Popen object before the translator Popen object
611 in PatchStreamForwarder, otherwise the `stdin=subprocess.PIPE` would cause
612 trouble to the translator pipe (select() never see EOF after input stream
613 ended), most likely python bug 12607 (http://bugs.python.org/issue12607)
614 which was fixed in python 2.7.3.
615
616 See issue #30 (https://github.com/ymattw/cdiff/issues/30) for more
617 information.
618 """
db3bc92b » rakuco
2013-12-10 Respect the `LESS' environment variable.
619 pager_cmd = ['less']
620 if not os.getenv('LESS'):
621 # Args stolen from git source: github.com/git/git/blob/master/pager.c
c95c027f »
2014-06-19 Enable smooth horizontal scrolling with less option `--shift 1`
622 pager_cmd.extend(['-FRSX', '--shift 1'])
17559700 »
2013-09-27 Adjust Popen object creation order to fix issue #30
623 pager = subprocess.Popen(
db3bc92b » rakuco
2013-12-10 Respect the `LESS' environment variable.
624 pager_cmd, stdin=subprocess.PIPE, stdout=sys.stdout)
17559700 »
2013-09-27 Adjust Popen object creation order to fix issue #30
625
04ac8953 »
2013-03-23 Refactor to make logic clearer, no DiffOps anymore
626 diffs = DiffParser(stream).get_diff_generator()
627 marker = DiffMarker()
628 color_diff = marker.markup(diffs, side_by_side=opts.side_by_side,
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
629 width=opts.width)
f9c4553b »
2013-02-02 ignore EPIPE happens when input patch set is large
630
2cb541c9 » myint
2013-08-23 Handle all keyboard interrupts more completely
631 for line in color_diff:
632 pager.stdin.write(line.encode('utf-8'))
f9c4553b »
2013-02-02 ignore EPIPE happens when input patch set is large
633
634 pager.stdin.close()
635 pager.wait()
636
637
cbf54f3e » myint
2013-02-02 Add support for Subversion
638 def check_command_status(arguments):
639 """Return True if command returns 0."""
bf7b7e16 » myint
2013-02-02 Add support for Mercurial
640 try:
641 return subprocess.call(
642 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0
643 except OSError:
644 return False
cbf54f3e » myint
2013-02-02 Add support for Subversion
645
646
75b79f09 »
2013-02-22 Support reading diff or log for given files/dirs in workspace (usage …
647 def revision_control_diff(args):
ed0ce1cb » myint
2013-02-02 Enable use as a revision control diff
648 """Return diff from revision control system."""
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
649 for _, ops in VCS_INFO.items():
650 if check_command_status(ops['probe']):
75b79f09 »
2013-02-22 Support reading diff or log for given files/dirs in workspace (usage …
651 return subprocess.Popen(
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
652 ops['diff'] + args, stdout=subprocess.PIPE).stdout
ed0ce1cb » myint
2013-02-02 Enable use as a revision control diff
653
654
75b79f09 »
2013-02-22 Support reading diff or log for given files/dirs in workspace (usage …
655 def revision_control_log(args):
93d2bf08 » myint
2013-02-18 Add "--log" option
656 """Return log from revision control system."""
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
657 for _, ops in VCS_INFO.items():
658 if check_command_status(ops['probe']):
75b79f09 »
2013-02-22 Support reading diff or log for given files/dirs in workspace (usage …
659 return subprocess.Popen(
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
660 ops['log'] + args, stdout=subprocess.PIPE).stdout
93d2bf08 » myint
2013-02-18 Add "--log" option
661
662
ed0ce1cb » myint
2013-02-02 Enable use as a revision control diff
663 def decode(line):
664 """Decode UTF-8 if necessary."""
09574025 » myint
2013-10-12 Handle Latin-1 encoded text in diffs
665 if isinstance(line, unicode):
ed0ce1cb » myint
2013-02-02 Enable use as a revision control diff
666 return line
667
09574025 » myint
2013-10-12 Handle Latin-1 encoded text in diffs
668 for encoding in ['utf-8', 'latin1']:
669 try:
670 return line.decode(encoding)
671 except UnicodeDecodeError:
672 pass
673
674 return '*** cdiff: undecodable bytes ***\n'
675
6befc2a7 » amigrave
2014-06-17 Add terminal size fit for side by side view
676
ac439e26 »
2014-06-17 Remove win32 support for detecting terminal size
677 def terminal_size():
678 """Returns terminal size. Taken from https://gist.github.com/marsam/7268750
679 but removed win32 support which depends on 3rd party extension.
6befc2a7 » amigrave
2014-06-17 Add terminal size fit for side by side view
680 """
681 width, height = None, None
ac439e26 »
2014-06-17 Remove win32 support for detecting terminal size
682 try:
683 import struct, fcntl, termios
684 s = struct.pack('HHHH', 0, 0, 0, 0)
685 x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
686 height, width = struct.unpack('HHHH', x)[0:2]
687 except (IOError, AttributeError):
688 pass
6befc2a7 » amigrave
2014-06-17 Add terminal size fit for side by side view
689 return width, height
690
ed0ce1cb » myint
2013-02-02 Enable use as a revision control diff
691
3aa7040b » myint
2013-02-03 Add missing main()
692 def main():
2cb541c9 » myint
2013-08-23 Handle all keyboard interrupts more completely
693 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
694 signal.signal(signal.SIGINT, signal.SIG_DFL)
695
5f671d7b »
2013-06-21 Show extra help message after option list
696 from optparse import (OptionParser, BadOptionError, AmbiguousOptionError,
697 OptionGroup)
57e62f2a »
2013-06-21 Stop on first unknown option and pass them down
698
699 class PassThroughOptionParser(OptionParser):
700 """Stop parsing on first unknown option (e.g. --cached, -U10) and pass
701 them down. Note the `opt_str` in exception object does not give us
702 chance to take the full option back, e.g. for '-U10' it will only
703 contain '-U' and the '10' part will be lost. Ref: http://goo.gl/IqY4A
704 (on stackoverflow). My hack is to try parse and insert a '--' in place
705 and parse again. Let me know if someone has better solution.
706 """
707 def _process_args(self, largs, rargs, values):
708 left = largs[:]
709 right = rargs[:]
710 try:
711 OptionParser._process_args(self, left, right, values)
712 except (BadOptionError, AmbiguousOptionError):
713 parsed_num = len(rargs) - len(right) - 1
714 rargs.insert(parsed_num, '--')
715 OptionParser._process_args(self, largs, rargs, values)
b02e23bb »
2013-01-29 initial revision
716
7debe071 »
2013-02-19 - Fixed incorrect yield on diff missing eof
717 supported_vcs = sorted(VCS_INFO.keys())
fd87da44 »
2013-02-03 Document/usage/dogfood test update after merge for issue #4 (Revision…
718
176bd406 »
2013-02-23 Document/usage update for 0.7
719 usage = """%prog [options] [file|dir ...]"""
57e62f2a »
2013-06-21 Stop on first unknown option and pass them down
720 parser = PassThroughOptionParser(
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
721 usage=usage, description=META_INFO['description'],
722 version='%%prog %s' % META_INFO['version'])
723 parser.add_option(
724 '-s', '--side-by-side', action='store_true',
725 help='enable side-by-side mode')
726 parser.add_option(
727 '-w', '--width', type='int', default=80, metavar='N',
a5b4e5eb »
2014-06-17 Usage update for --width=0
728 help='set text width for side-by-side mode, 0 for auto detection, '
729 'default is 80')
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
730 parser.add_option(
731 '-l', '--log', action='store_true',
732 help='show log with changes from revision control')
733 parser.add_option(
56eb7bce »
2013-03-23 Version info and document update for 0.9
734 '-c', '--color', default='auto', metavar='M',
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
735 help="""colorize mode 'auto' (default), 'always', or 'never'""")
5f671d7b »
2013-06-21 Show extra help message after option list
736
737 # Hack: use OptionGroup text for extra help message after option list
738 option_group = OptionGroup(
739 parser, "Note", ("Option parser will stop on first unknown option "
740 "and pass them down to underneath revision control"))
741 parser.add_option_group(option_group)
742
b02e23bb »
2013-01-29 initial revision
743 opts, args = parser.parse_args()
744
93d2bf08 » myint
2013-02-18 Add "--log" option
745 if opts.log:
75b79f09 »
2013-02-22 Support reading diff or log for given files/dirs in workspace (usage …
746 diff_hdl = revision_control_log(args)
93d2bf08 » myint
2013-02-18 Add "--log" option
747 if not diff_hdl:
748 sys.stderr.write(('*** Not in a supported workspace, supported '
590fe005 » myint
2013-02-18 Don't print help in "--log"
749 'are: %s\n') % ', '.join(supported_vcs))
93d2bf08 » myint
2013-02-18 Add "--log" option
750 return 1
b02e23bb »
2013-01-29 initial revision
751 elif sys.stdin.isatty():
75b79f09 »
2013-02-22 Support reading diff or log for given files/dirs in workspace (usage …
752 diff_hdl = revision_control_diff(args)
ed0ce1cb » myint
2013-02-02 Enable use as a revision control diff
753 if not diff_hdl:
fd87da44 »
2013-02-03 Document/usage/dogfood test update after merge for issue #4 (Revision…
754 sys.stderr.write(('*** Not in a supported workspace, supported '
755 'are: %s\n\n') % ', '.join(supported_vcs))
ed0ce1cb » myint
2013-02-02 Enable use as a revision control diff
756 parser.print_help()
00d946cb »
2013-02-04 Use return in main(), let prog has one exit point
757 return 1
b02e23bb »
2013-01-29 initial revision
758 else:
09574025 » myint
2013-10-12 Handle Latin-1 encoded text in diffs
759 diff_hdl = (sys.stdin.buffer if hasattr(sys.stdin, 'buffer')
760 else sys.stdin)
b02e23bb »
2013-01-29 initial revision
761
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
762 stream = PatchStream(diff_hdl)
abe1787c »
2013-02-07 Support compare two files (wrapper of diff)
763
59e21fcf »
2013-01-31 better _fit_width(); FIXME: out[-1] might broken, see diff of this co…
764 # Don't let empty diff pass thru
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
765 if stream.is_empty():
00d946cb »
2013-02-04 Use return in main(), let prog has one exit point
766 return 0
59e21fcf »
2013-01-31 better _fit_width(); FIXME: out[-1] might broken, see diff of this co…
767
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
768 if opts.color == 'always' or \
769 (opts.color == 'auto' and sys.stdout.isatty()):
d3032476 » myint
2013-08-29 Remove code made unnecessary by #28
770 markup_to_pager(stream, opts)
b02e23bb »
2013-01-29 initial revision
771 else:
92061d31 »
2013-01-30 massive refactor with difflib._mdiff
772 # pipe out stream untouched to make sure it is still a patch
09574025 » myint
2013-10-12 Handle Latin-1 encoded text in diffs
773 byte_output = (sys.stdout.buffer if hasattr(sys.stdout, 'buffer')
774 else sys.stdout)
4c83c806 » myint
2013-02-18 Handle large diffs in non-color mode
775 for line in stream:
09574025 » myint
2013-10-12 Handle Latin-1 encoded text in diffs
776 byte_output.write(line)
b02e23bb »
2013-01-29 initial revision
777
3fcd0625 »
2013-02-18 Use generator to deal with large patch stream
778 if diff_hdl is not sys.stdin:
779 diff_hdl.close()
780
00d946cb »
2013-02-04 Use return in main(), let prog has one exit point
781 return 0
782
3aa7040b » myint
2013-02-03 Add missing main()
783
784 if __name__ == '__main__':
00d946cb »
2013-02-04 Use return in main(), let prog has one exit point
785 sys.exit(main())
b02e23bb »
2013-01-29 initial revision
786
f64e6ceb »
2013-02-25 Conform PEP8 (with minor own flavors)
787 # vim:set et sts=4 sw=4 tw=79:
Something went wrong with that request. Please try again.