瀏覽代碼

massive refactor with difflib._mdiff

Matthew Wang 11 年之前
父節點
當前提交
92061d3100
共有 2 個文件被更改,包括 163 次插入139 次删除
  1. 4
    4
      Makefile
  2. 159
    135
      src/cdiff.py

+ 4
- 4
Makefile 查看文件

@@ -5,11 +5,11 @@
5 5
 test: single-udiff multi-udiff
6 6
 
7 7
 single-udiff:
8
-	src/cdiff.py -t tests/single.udiff
9
-	src/cdiff.py -t tests/single.udiff | diff -u tests/single.udiff -
8
+	src/cdiff.py tests/single.udiff
9
+	src/cdiff.py tests/single.udiff | diff -u tests/single.udiff -
10 10
 
11 11
 multi-udiff:
12
-	src/cdiff.py -t tests/multi.udiff
13
-	src/cdiff.py -t tests/multi.udiff | diff -u tests/multi.udiff -
12
+	src/cdiff.py tests/multi.udiff
13
+	src/cdiff.py tests/multi.udiff | diff -u tests/multi.udiff -
14 14
 
15 15
 # vim:set noet ts=8 sw=8:

+ 159
- 135
src/cdiff.py 查看文件

@@ -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