|
@@ -3,138 +3,165 @@
|
3
|
3
|
import sys
|
4
|
4
|
import os
|
5
|
5
|
import re
|
|
6
|
+import difflib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+COLORS = {
|
|
10
|
+ 'reset' : '\x1b[0m',
|
|
11
|
+ 'red' : '\x1b[31m',
|
|
12
|
+ 'green' : '\x1b[32m',
|
|
13
|
+ 'yellow' : '\x1b[33m',
|
|
14
|
+ 'blue' : '\x1b[34m',
|
|
15
|
+ 'magenta' : '\x1b[35m',
|
|
16
|
+ 'cyan' : '\x1b[36m',
|
|
17
|
+ 'lightred' : '\x1b[1;31m',
|
|
18
|
+ 'lightgreen' : '\x1b[1;32m',
|
|
19
|
+ 'lightyellow' : '\x1b[1;33m',
|
|
20
|
+ 'lightblue ' : '\x1b[1;34m',
|
|
21
|
+ 'lightmagenta' : '\x1b[1;35m',
|
|
22
|
+ 'lightcyan' : '\x1b[1;36m',
|
|
23
|
+}
|
|
24
|
+
|
|
25
|
+def ansi_code(color):
|
|
26
|
+ return COLORS.get(color, '')
|
|
27
|
+
|
|
28
|
+def colorize(text, start_color, end_color='reset'):
|
|
29
|
+ return ansi_code(start_color) + text + ansi_code(end_color)
|
6
|
30
|
|
7
|
31
|
|
8
|
32
|
class Hunk(object):
|
9
|
33
|
|
10
|
34
|
def __init__(self, hunk_header, old_addr, old_offset, new_addr, new_offset):
|
11
|
|
- self.__hunk_header = hunk_header
|
12
|
|
- self.__old_addr = old_addr
|
13
|
|
- self.__old_offset = old_offset
|
14
|
|
- self.__new_addr = new_addr
|
15
|
|
- self.__new_offset = new_offset
|
16
|
|
- self.__hunk_list = [] # 2-element group (attr, line)
|
|
35
|
+ self._hunk_header = hunk_header
|
|
36
|
+ self._old_addr = old_addr
|
|
37
|
+ self._old_offset = old_offset
|
|
38
|
+ self._new_addr = new_addr
|
|
39
|
+ self._new_offset = new_offset
|
|
40
|
+ self._hunk_list = [] # 2-element group (attr, line)
|
17
|
41
|
|
18
|
42
|
def get_header(self):
|
19
|
|
- return self.__hunk_header
|
|
43
|
+ return self._hunk_header
|
20
|
44
|
|
21
|
45
|
def get_old_addr(self):
|
22
|
|
- return (self.__old_addr, self.__old_offset)
|
|
46
|
+ return (self._old_addr, self._old_offset)
|
23
|
47
|
|
24
|
48
|
def get_new_addr(self):
|
25
|
|
- return (self.__new_addr, self.__new_offset)
|
|
49
|
+ return (self._new_addr, self._new_offset)
|
26
|
50
|
|
27
|
51
|
def append(self, attr, line):
|
28
|
52
|
"""attr: '-': old, '+': new, ' ': common"""
|
29
|
|
- self.__hunk_list.append((attr, line))
|
|
53
|
+ self._hunk_list.append((attr, line))
|
|
54
|
+
|
|
55
|
+ def mdiff(self):
|
|
56
|
+ """The difflib._mdiff() function returns an interator which returns a
|
|
57
|
+ tuple: (from line tuple, to line tuple, boolean flag)
|
|
58
|
+
|
|
59
|
+ from/to line tuple -- (line num, line text)
|
|
60
|
+ line num -- integer or None (to indicate a context separation)
|
|
61
|
+ line text -- original line text with following markers inserted:
|
|
62
|
+ '\0+' -- marks start of added text
|
|
63
|
+ '\0-' -- marks start of deleted text
|
|
64
|
+ '\0^' -- marks start of changed text
|
|
65
|
+ '\1' -- marks end of added/deleted/changed text
|
|
66
|
+
|
|
67
|
+ boolean flag -- None indicates context separation, True indicates
|
|
68
|
+ either "from" or "to" line contains a change, otherwise False.
|
|
69
|
+ """
|
|
70
|
+ return difflib._mdiff(self._get_old_text(), self._get_new_text())
|
|
71
|
+
|
|
72
|
+ def _get_old_text(self):
|
|
73
|
+ out = []
|
|
74
|
+ for (attr, line) in self._hunk_list:
|
|
75
|
+ if attr != '+':
|
|
76
|
+ out.append(line)
|
|
77
|
+ return out
|
|
78
|
+
|
|
79
|
+ def _get_new_text(self):
|
|
80
|
+ out = []
|
|
81
|
+ for (attr, line) in self._hunk_list:
|
|
82
|
+ if attr != '-':
|
|
83
|
+ out.append(line)
|
|
84
|
+ return out
|
30
|
85
|
|
31
|
86
|
def __iter__(self):
|
32
|
|
- for hunk_line in self.__hunk_list:
|
|
87
|
+ for hunk_line in self._hunk_list:
|
33
|
88
|
yield hunk_line
|
34
|
89
|
|
35
|
90
|
|
36
|
91
|
class Diff(object):
|
37
|
92
|
|
38
|
93
|
def __init__(self, headers, old_path, new_path, hunks):
|
39
|
|
- self.__headers = headers
|
40
|
|
- self.__old_path = old_path
|
41
|
|
- self.__new_path = new_path
|
42
|
|
- self.__hunks = hunks
|
|
94
|
+ self._headers = headers
|
|
95
|
+ self._old_path = old_path
|
|
96
|
+ self._new_path = new_path
|
|
97
|
+ self._hunks = hunks
|
43
|
98
|
|
44
|
|
- def render_traditional(self, show_color):
|
|
99
|
+ def markup_traditional(self):
|
45
|
100
|
out = []
|
46
|
|
- if show_color:
|
47
|
|
- color = None # Use default
|
48
|
|
- end_color = None
|
49
|
|
- else:
|
50
|
|
- color = 'none' # No color
|
51
|
|
- end_color = 'none'
|
52
|
|
-
|
53
|
|
- for line in self.__headers:
|
54
|
|
- out.append(self._render_header(line, color, end_color))
|
55
|
|
-
|
56
|
|
- out.append(self._render_old_path(self.__old_path, color, end_color))
|
57
|
|
- out.append(self._render_new_path(self.__new_path, color, end_color))
|
58
|
|
-
|
59
|
|
- for hunk in self.__hunks:
|
60
|
|
- out.append(self._render_hunk_header(hunk.get_header(), color,
|
61
|
|
- end_color))
|
62
|
|
- for (attr, line) in hunk:
|
63
|
|
- if attr == '-':
|
64
|
|
- out.append(self._render_old(attr+line, color, end_color))
|
65
|
|
- elif attr == '+':
|
66
|
|
- out.append(self._render_new(attr+line, color, end_color))
|
67
|
|
- else:
|
68
|
|
- out.append(self._render_common(attr+line, color, end_color))
|
69
|
101
|
|
|
102
|
+ for line in self._headers:
|
|
103
|
+ out.append(self._markup_header(line))
|
|
104
|
+
|
|
105
|
+ out.append(self._markup_old_path(self._old_path))
|
|
106
|
+ out.append(self._markup_new_path(self._new_path))
|
|
107
|
+
|
|
108
|
+ for hunk in self._hunks:
|
|
109
|
+ out.append(self._markup_hunk_header(hunk.get_header()))
|
|
110
|
+ save_line = ''
|
|
111
|
+ for from_info, to_info, changed in hunk.mdiff():
|
|
112
|
+ if changed:
|
|
113
|
+ if not from_info[0]:
|
|
114
|
+ line = to_info[1].strip('\x00\x01')
|
|
115
|
+ out.append(self._markup_new(line))
|
|
116
|
+ elif not to_info[0]:
|
|
117
|
+ line = from_info[1].strip('\x00\x01')
|
|
118
|
+ out.append(self._markup_old(line))
|
|
119
|
+ else:
|
|
120
|
+ out.append(self._markup_old('-' +
|
|
121
|
+ self._markup_old_mix(from_info[1])))
|
|
122
|
+ out.append(self._markup_new('+' +
|
|
123
|
+ self._markup_new_mix(to_info[1])))
|
|
124
|
+ else:
|
|
125
|
+ out.append(self._markup_common(' ' + from_info[1]))
|
70
|
126
|
return ''.join(out)
|
71
|
127
|
|
72
|
|
- def render_side_by_side(self, show_color, show_number, width):
|
|
128
|
+ def markup_side_by_side(self, show_number, width):
|
73
|
129
|
"""Do not really need to parse the hunks..."""
|
74
|
|
- return 'TODO: show_color=%s, show_number=%s, width=%d' % (show_color,
|
75
|
|
- show_number, width)
|
76
|
|
-
|
77
|
|
- def _render_header(self, line, color=None, end_color=None):
|
78
|
|
- if color is None:
|
79
|
|
- color='cyan'
|
80
|
|
- if end_color is None:
|
81
|
|
- end_color = 'reset'
|
82
|
|
- return self.__mark_color(line, color, end_color)
|
83
|
|
-
|
84
|
|
- def _render_old_path(self, line, color=None, end_color=None):
|
85
|
|
- if color is None:
|
86
|
|
- color='yellow'
|
87
|
|
- if end_color is None:
|
88
|
|
- end_color = 'reset'
|
89
|
|
- return self.__mark_color(line, color, end_color)
|
90
|
|
-
|
91
|
|
- def _render_new_path(self, line, color=None, end_color=None):
|
92
|
|
- if color is None:
|
93
|
|
- color='yellow'
|
94
|
|
- if end_color is None:
|
95
|
|
- end_color = 'reset'
|
96
|
|
- return self.__mark_color(line, color, end_color)
|
97
|
|
-
|
98
|
|
- def _render_hunk_header(self, line, color=None, end_color=None):
|
99
|
|
- if color is None:
|
100
|
|
- color='lightblue'
|
101
|
|
- if end_color is None:
|
102
|
|
- end_color = 'reset'
|
103
|
|
- return self.__mark_color(line, color, end_color)
|
104
|
|
-
|
105
|
|
- def _render_old(self, line, color=None, end_color=None):
|
106
|
|
- if color is None:
|
107
|
|
- color='red'
|
108
|
|
- if end_color is None:
|
109
|
|
- end_color = 'reset'
|
110
|
|
- return self.__mark_color(line, color, end_color)
|
111
|
|
-
|
112
|
|
- def _render_new(self, line, color=None, end_color=None):
|
113
|
|
- if color is None:
|
114
|
|
- color='green'
|
115
|
|
- if end_color is None:
|
116
|
|
- end_color = 'reset'
|
117
|
|
- return self.__mark_color(line, color, end_color)
|
118
|
|
-
|
119
|
|
- def _render_common(self, line, color=None, end_color=None):
|
120
|
|
- if color is None:
|
121
|
|
- color='none'
|
122
|
|
- if end_color is None:
|
123
|
|
- end_color = 'none'
|
124
|
|
- return self.__mark_color(line, color, end_color)
|
125
|
|
-
|
126
|
|
- def __mark_color(self, text, start_code, end_code):
|
127
|
|
- colors = {
|
128
|
|
- 'none' : '',
|
129
|
|
- 'reset' : '\x1b[0m',
|
130
|
|
- 'red' : '\x1b[31m',
|
131
|
|
- 'green' : '\x1b[32m',
|
132
|
|
- 'yellow' : '\x1b[33m',
|
133
|
|
- 'blue' : '\x1b[34m',
|
134
|
|
- 'cyan' : '\x1b[36m',
|
135
|
|
- 'lightblue' : '\x1b[1;34m',
|
136
|
|
- }
|
137
|
|
- return colors.get(start_code) + text + colors.get(end_code)
|
|
130
|
+ return 'TODO: show_number=%s, width=%d' % (show_number, width)
|
|
131
|
+
|
|
132
|
+ def _markup_header(self, line):
|
|
133
|
+ return colorize(line, 'cyan')
|
|
134
|
+
|
|
135
|
+ def _markup_old_path(self, line):
|
|
136
|
+ return colorize(line, 'yellow')
|
|
137
|
+
|
|
138
|
+ def _markup_new_path(self, line):
|
|
139
|
+ return colorize(line, 'yellow')
|
|
140
|
+
|
|
141
|
+ def _markup_hunk_header(self, line):
|
|
142
|
+ return colorize(line, 'blue')
|
|
143
|
+
|
|
144
|
+ def _markup_common(self, line):
|
|
145
|
+ return colorize(line, 'reset')
|
|
146
|
+
|
|
147
|
+ def _markup_old(self, line):
|
|
148
|
+ return colorize(line, 'lightred')
|
|
149
|
+
|
|
150
|
+ def _markup_new(self, line):
|
|
151
|
+ return colorize(line, 'lightgreen')
|
|
152
|
+
|
|
153
|
+ def _markup_mix(self, line, end_color):
|
|
154
|
+ line = line.replace('\x00-', ansi_code('red'))
|
|
155
|
+ line = line.replace('\x00+', ansi_code('green'))
|
|
156
|
+ line = line.replace('\x00^', ansi_code('lightyellow'))
|
|
157
|
+ line = line.replace('\x01', ansi_code(end_color))
|
|
158
|
+ return colorize(line, end_color)
|
|
159
|
+
|
|
160
|
+ def _markup_old_mix(self, line):
|
|
161
|
+ return self._markup_mix(line, 'red')
|
|
162
|
+
|
|
163
|
+ def _markup_new_mix(self, line):
|
|
164
|
+ return self._markup_mix(line, 'green')
|
138
|
165
|
|
139
|
166
|
|
140
|
167
|
class Udiff(Diff):
|
|
@@ -178,27 +205,27 @@ class DiffParser(object):
|
178
|
205
|
def __init__(self, stream):
|
179
|
206
|
for line in stream[:10]:
|
180
|
207
|
if line.startswith('+++ '):
|
181
|
|
- self.__type = 'udiff'
|
|
208
|
+ self._type = 'udiff'
|
182
|
209
|
break
|
183
|
210
|
else:
|
184
|
211
|
raise RuntimeError('unknown diff type')
|
185
|
212
|
|
186
|
213
|
try:
|
187
|
|
- self.__diffs = self.__parse(stream)
|
|
214
|
+ self._diffs = self._parse(stream)
|
188
|
215
|
except (AssertionError, IndexError):
|
189
|
216
|
raise RuntimeError('invalid patch format')
|
190
|
217
|
|
191
|
218
|
|
192
|
219
|
def get_diffs(self):
|
193
|
|
- return self.__diffs
|
|
220
|
+ return self._diffs
|
194
|
221
|
|
195
|
|
- def __parse(self, stream):
|
196
|
|
- if self.__type == 'udiff':
|
197
|
|
- return self.__parse_udiff(stream)
|
|
222
|
+ def _parse(self, stream):
|
|
223
|
+ if self._type == 'udiff':
|
|
224
|
+ return self._parse_udiff(stream)
|
198
|
225
|
else:
|
199
|
226
|
raise RuntimeError('unsupported diff format')
|
200
|
227
|
|
201
|
|
- def __parse_udiff(self, stream):
|
|
228
|
+ def _parse_udiff(self, stream):
|
202
|
229
|
"""parse all diff lines here, construct a list of Udiff objects"""
|
203
|
230
|
out_diffs = []
|
204
|
231
|
headers = []
|
|
@@ -297,29 +324,28 @@ class DiffParser(object):
|
297
|
324
|
return out_diffs
|
298
|
325
|
|
299
|
326
|
|
300
|
|
-class DiffRender(object):
|
|
327
|
+class DiffMarkup(object):
|
301
|
328
|
|
302
|
329
|
def __init__(self, stream):
|
303
|
|
- self.__diffs = DiffParser(stream).get_diffs()
|
|
330
|
+ self._diffs = DiffParser(stream).get_diffs()
|
304
|
331
|
|
305
|
|
- def render(self, show_color=True, show_number=False, width=0,
|
306
|
|
- traditional=False):
|
307
|
|
- if traditional:
|
308
|
|
- return self.__render_traditional(show_color)
|
|
332
|
+ def markup(self, side_by_side=False, show_number=False, width=0):
|
|
333
|
+ if side_by_side:
|
|
334
|
+ return self._markup_side_by_side(show_number, width)
|
309
|
335
|
else:
|
310
|
|
- return self.__render_side_by_side(show_color, show_number, width)
|
|
336
|
+ return self._markup_traditional()
|
311
|
337
|
|
312
|
|
- def __render_traditional(self, show_color):
|
|
338
|
+ def _markup_traditional(self):
|
313
|
339
|
out = []
|
314
|
|
- for diff in self.__diffs:
|
315
|
|
- out.append(diff.render_traditional(show_color))
|
|
340
|
+ for diff in self._diffs:
|
|
341
|
+ out.append(diff.markup_traditional())
|
316
|
342
|
return out
|
317
|
343
|
|
318
|
|
- def __render_side_by_side(self, show_color, show_number, width):
|
|
344
|
+ def _markup_side_by_side(self, show_number, width):
|
319
|
345
|
"""width of 0 or negative means auto detect terminal width"""
|
320
|
346
|
out = []
|
321
|
|
- for diff in self.__diffs:
|
322
|
|
- out.append(diff.render_side_by_side(show_color, show_number, width))
|
|
347
|
+ for diff in self._diffs:
|
|
348
|
+ out.append(diff.markup_side_by_side(show_number, width))
|
323
|
349
|
return out
|
324
|
350
|
|
325
|
351
|
|
|
@@ -334,17 +360,14 @@ if __name__ == '__main__':
|
334
|
360
|
{'prog': os.path.basename(sys.argv[0])}
|
335
|
361
|
|
336
|
362
|
parser = optparse.OptionParser(usage)
|
|
363
|
+ parser.add_option('-s', '--side-by-side', action='store_true',
|
|
364
|
+ help=('show in side-by-side mode'))
|
337
|
365
|
parser.add_option('-n', '--number', action='store_true',
|
338
|
366
|
help='show line number')
|
339
|
367
|
parser.add_option('-w', '--width', type='int', default=0,
|
340
|
|
- help='set text width for each side')
|
341
|
|
- parser.add_option('-t', '--traditional', action='store_true',
|
342
|
|
- help=('show in traditional format other than default side-by-side '
|
343
|
|
- 'mode (omit -n, -w)'))
|
|
368
|
+ help='set line width (side-by-side mode only)')
|
344
|
369
|
opts, args = parser.parse_args()
|
345
|
370
|
|
346
|
|
- show_color = sys.stdout.isatty()
|
347
|
|
-
|
348
|
371
|
if opts.width < 0:
|
349
|
372
|
opts.width = 0
|
350
|
373
|
|
|
@@ -360,11 +383,11 @@ if __name__ == '__main__':
|
360
|
383
|
if diff_hdl is not sys.stdin:
|
361
|
384
|
diff_hdl.close()
|
362
|
385
|
|
363
|
|
- render = DiffRender(stream)
|
364
|
|
- color_diff = render.render(show_color=show_color, show_number=opts.number,
|
365
|
|
- width=opts.width, traditional=opts.traditional)
|
366
|
|
-
|
367
|
386
|
if sys.stdout.isatty():
|
|
387
|
+ markup = DiffMarkup(stream)
|
|
388
|
+ color_diff = markup.markup(side_by_side=opts.side_by_side,
|
|
389
|
+ show_number=opts.number, width=opts.width)
|
|
390
|
+
|
368
|
391
|
# args stolen fron git source: github.com/git/git/blob/master/pager.c
|
369
|
392
|
pager = subprocess.Popen(['less', '-FRSXK'],
|
370
|
393
|
stdin=subprocess.PIPE, stdout=sys.stdout)
|
|
@@ -372,7 +395,8 @@ if __name__ == '__main__':
|
372
|
395
|
pager.stdin.close()
|
373
|
396
|
pager.wait()
|
374
|
397
|
else:
|
375
|
|
- sys.stdout.write(''.join(color_diff))
|
|
398
|
+ # pipe out stream untouched to make sure it is still a patch
|
|
399
|
+ sys.stdout.write(''.join(stream))
|
376
|
400
|
|
377
|
401
|
sys.exit(0)
|
378
|
402
|
|