|
@@ -2,13 +2,13 @@
|
2
|
2
|
|
3
|
3
|
|
4
|
4
|
"""
|
5
|
|
-Term based tool to view **colored**, **incremental** diff in *git/svn/hg*
|
|
5
|
+Term based tool to view **colored**, **incremental** diff in Git/Mercurial/Svn
|
6
|
6
|
workspace, given patch or two files, or from stdin, with **side by side** and
|
7
|
7
|
**auto pager** support. Requires python (>= 2.5.0) and ``less``.
|
8
|
8
|
"""
|
9
|
9
|
|
10
|
10
|
META_INFO = {
|
11
|
|
- 'version' : '0.5',
|
|
11
|
+ 'version' : '0.5.1',
|
12
|
12
|
'license' : 'BSD-3',
|
13
|
13
|
'author' : 'Matthew Wang',
|
14
|
14
|
'email' : 'mattwyl(@)gmail(.)com',
|
|
@@ -50,12 +50,24 @@ COLORS = {
|
50
|
50
|
}
|
51
|
51
|
|
52
|
52
|
|
53
|
|
-
|
54
|
|
-REVISION_CONTROL = (
|
55
|
|
- (['git', 'rev-parse'], ['git', 'diff'], ['git', 'log', '--patch']),
|
56
|
|
- (['svn', 'info'], ['svn', 'diff'], ['svn', 'log', '--diff']),
|
57
|
|
- (['hg', 'summary'], ['hg', 'diff'], ['hg', 'log', '--patch'])
|
58
|
|
-)
|
|
53
|
+
|
|
54
|
+VCS_INFO = {
|
|
55
|
+ 'Git': {
|
|
56
|
+ 'probe' : ['git', 'rev-parse'],
|
|
57
|
+ 'diff' : ['git', 'diff'],
|
|
58
|
+ 'log' : ['git', 'log', '--patch'],
|
|
59
|
+ },
|
|
60
|
+ 'Mercurial': {
|
|
61
|
+ 'probe' : ['hg', 'summary'],
|
|
62
|
+ 'diff' : ['hg', 'diff'],
|
|
63
|
+ 'log' : ['hg', 'log', '--patch'],
|
|
64
|
+ },
|
|
65
|
+ 'Svn': {
|
|
66
|
+ 'probe' : ['svn', 'info'],
|
|
67
|
+ 'diff' : ['svn', 'diff'],
|
|
68
|
+ 'log' : ['svn', 'log', '--diff'],
|
|
69
|
+ },
|
|
70
|
+}
|
59
|
71
|
|
60
|
72
|
|
61
|
73
|
def ansi_code(color):
|
|
@@ -86,9 +98,10 @@ class Hunk(object):
|
86
|
98
|
def get_new_addr(self):
|
87
|
99
|
return self._new_addr
|
88
|
100
|
|
89
|
|
- def append(self, attr, line):
|
90
|
|
- """attr: '-': old, '+': new, ' ': common"""
|
91
|
|
- self._hunk_list.append((attr, line))
|
|
101
|
+ def append(self, hunk_line):
|
|
102
|
+ """hunk_line is a 2-element tuple: (attr, text), where attris : '-':
|
|
103
|
+ old, '+': new, ' ': common"""
|
|
104
|
+ self._hunk_list.append(hunk_line)
|
92
|
105
|
|
93
|
106
|
def mdiff(self):
|
94
|
107
|
r"""The difflib._mdiff() function returns an interator which returns a
|
|
@@ -134,9 +147,9 @@ class Diff(object):
|
134
|
147
|
self._new_path = new_path
|
135
|
148
|
self._hunks = hunks
|
136
|
149
|
|
137
|
|
-
|
138
|
|
-
|
139
|
|
-
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
140
|
153
|
|
141
|
154
|
def is_old_path(self, line):
|
142
|
155
|
return False
|
|
@@ -148,9 +161,14 @@ class Diff(object):
|
148
|
161
|
return False
|
149
|
162
|
|
150
|
163
|
def parse_hunk_meta(self, line):
|
151
|
|
- """Returns a 2-eliment tuple, each of them is a tuple in form of (start,
|
|
164
|
+ """Returns a 2-element tuple, each of them is a tuple in form of (start,
|
152
|
165
|
offset)"""
|
153
|
|
- return False
|
|
166
|
+ return None
|
|
167
|
+
|
|
168
|
+ def parse_hunk_line(self, line):
|
|
169
|
+ """Returns a 2-element tuple: (attr, text), where attr is: '-': old,
|
|
170
|
+ '+': new, ' ': common"""
|
|
171
|
+ return None
|
154
|
172
|
|
155
|
173
|
def is_old(self, line):
|
156
|
174
|
return False
|
|
@@ -203,8 +221,9 @@ class Diff(object):
|
203
|
221
|
return line.replace('\t', ' '*8).replace('\n', '').replace('\r', '')
|
204
|
222
|
|
205
|
223
|
def _fit_width(markup, width, pad=False):
|
206
|
|
- """str len does not count correctly if left column contains ansi
|
207
|
|
- color code. Only left side need to set `pad`
|
|
224
|
+ """Fit input markup to given width, pad or wrap accordingly, str len
|
|
225
|
+ does not count correctly if line contains ansi color code. Only
|
|
226
|
+ left side need to set `pad`
|
208
|
227
|
"""
|
209
|
228
|
out = []
|
210
|
229
|
count = 0
|
|
@@ -214,6 +233,9 @@ class Diff(object):
|
214
|
233
|
|
215
|
234
|
while markup and count < width:
|
216
|
235
|
if patt.match(markup):
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
217
|
239
|
out.append(patt.sub(r'\1', markup))
|
218
|
240
|
markup = patt.sub(r'\3', markup)
|
219
|
241
|
else:
|
|
@@ -227,7 +249,7 @@ class Diff(object):
|
227
|
249
|
markup = markup[1:]
|
228
|
250
|
|
229
|
251
|
if count == width and repl.sub('', markup):
|
230
|
|
-
|
|
252
|
+
|
231
|
253
|
out[-1] = ansi_code('reset') + colorize('>', 'lightmagenta')
|
232
|
254
|
elif count < width and pad:
|
233
|
255
|
pad_len = width - count
|
|
@@ -366,6 +388,9 @@ class Udiff(Diff):
|
366
|
388
|
|
367
|
389
|
return (old_addr, new_addr)
|
368
|
390
|
|
|
391
|
+ def parse_hunk_line(self, line):
|
|
392
|
+ return (line[0], line[1:])
|
|
393
|
+
|
369
|
394
|
def is_old(self, line):
|
370
|
395
|
"""Exclude header line from svn log --diff output"""
|
371
|
396
|
return line.startswith('-') and not self.is_old_path(line) and \
|
|
@@ -387,33 +412,33 @@ class PatchStream(object):
|
387
|
412
|
|
388
|
413
|
def __init__(self, diff_hdl):
|
389
|
414
|
self._diff_hdl = diff_hdl
|
390
|
|
- self._header_chunk_size = 0
|
391
|
|
- self._header_chunk = []
|
|
415
|
+ self._stream_header_size = 0
|
|
416
|
+ self._stream_header = []
|
392
|
417
|
|
393
|
418
|
|
394
|
419
|
line = self._diff_hdl.readline()
|
395
|
|
- if line is None:
|
|
420
|
+ if not line:
|
396
|
421
|
self._is_empty = True
|
397
|
422
|
else:
|
398
|
|
- self._header_chunk.append(line)
|
399
|
|
- self._header_chunk_size += 1
|
|
423
|
+ self._stream_header.append(line)
|
|
424
|
+ self._stream_header_size += 1
|
400
|
425
|
self._is_empty = False
|
401
|
426
|
|
402
|
427
|
def is_empty(self):
|
403
|
428
|
return self._is_empty
|
404
|
429
|
|
405
|
|
- def read_header_chunks(self, header_chunk_size):
|
|
430
|
+ def read_stream_header(self, stream_header_size):
|
406
|
431
|
"""Returns a small chunk for patch type detect, suppose to call once"""
|
407
|
|
- for i in range(1, header_chunk_size):
|
|
432
|
+ for i in range(1, stream_header_size):
|
408
|
433
|
line = self._diff_hdl.readline()
|
409
|
|
- if line is None:
|
|
434
|
+ if not line:
|
410
|
435
|
break
|
411
|
|
- self._header_chunk.append(line)
|
412
|
|
- self._header_chunk_size += 1
|
413
|
|
- yield line
|
|
436
|
+ self._stream_header.append(line)
|
|
437
|
+ self._stream_header_size += 1
|
|
438
|
+ return self._stream_header
|
414
|
439
|
|
415
|
440
|
def __iter__(self):
|
416
|
|
- for line in self._header_chunk:
|
|
441
|
+ for line in self._stream_header:
|
417
|
442
|
yield line
|
418
|
443
|
for line in self._diff_hdl:
|
419
|
444
|
yield line
|
|
@@ -428,7 +453,7 @@ class DiffParser(object):
|
428
|
453
|
self._stream = stream
|
429
|
454
|
|
430
|
455
|
flag = 0
|
431
|
|
- for line in self._stream.read_header_chunks(100):
|
|
456
|
+ for line in self._stream.read_stream_header(100):
|
432
|
457
|
line = decode(line)
|
433
|
458
|
if line.startswith('--- '):
|
434
|
459
|
flag |= 1
|
|
@@ -436,7 +461,7 @@ class DiffParser(object):
|
436
|
461
|
flag |= 2
|
437
|
462
|
elif line.startswith('@@ ') or line.startswith('## '):
|
438
|
463
|
flag |= 4
|
439
|
|
- if flag & 7:
|
|
464
|
+ if (flag & 7) == 7:
|
440
|
465
|
self._type = 'udiff'
|
441
|
466
|
break
|
442
|
467
|
else:
|
|
@@ -481,8 +506,7 @@ class DiffParser(object):
|
481
|
506
|
|
482
|
507
|
elif len(diff._hunks) > 0 and (difflet.is_old(line) or \
|
483
|
508
|
difflet.is_new(line) or difflet.is_common(line)):
|
484
|
|
- hunk_line = line
|
485
|
|
- diff._hunks[-1].append(hunk_line[0], hunk_line[1:])
|
|
509
|
+ diff._hunks[-1].append(difflet.parse_hunk_line(line))
|
486
|
510
|
|
487
|
511
|
elif difflet.is_eof(line):
|
488
|
512
|
|
|
@@ -557,16 +581,16 @@ def check_command_status(arguments):
|
557
|
581
|
|
558
|
582
|
def revision_control_diff():
|
559
|
583
|
"""Return diff from revision control system."""
|
560
|
|
- for check, diff, _ in REVISION_CONTROL:
|
561
|
|
- if check_command_status(check):
|
562
|
|
- return subprocess.Popen(diff, stdout=subprocess.PIPE).stdout
|
|
584
|
+ for _, ops in VCS_INFO.items():
|
|
585
|
+ if check_command_status(ops['probe']):
|
|
586
|
+ return subprocess.Popen(ops['diff'], stdout=subprocess.PIPE).stdout
|
563
|
587
|
|
564
|
588
|
|
565
|
589
|
def revision_control_log():
|
566
|
590
|
"""Return log from revision control system."""
|
567
|
|
- for check, _, log in REVISION_CONTROL:
|
568
|
|
- if check_command_status(check):
|
569
|
|
- return subprocess.Popen(log, stdout=subprocess.PIPE).stdout
|
|
591
|
+ for _, ops in VCS_INFO.items():
|
|
592
|
+ if check_command_status(ops['probe']):
|
|
593
|
+ return subprocess.Popen(ops['log'], stdout=subprocess.PIPE).stdout
|
570
|
594
|
|
571
|
595
|
|
572
|
596
|
def decode(line):
|
|
@@ -580,7 +604,7 @@ def decode(line):
|
580
|
604
|
def main():
|
581
|
605
|
import optparse
|
582
|
606
|
|
583
|
|
- supported_vcs = [check[0][0] for check in REVISION_CONTROL]
|
|
607
|
+ supported_vcs = sorted(VCS_INFO.keys())
|
584
|
608
|
|
585
|
609
|
usage = """
|
586
|
610
|
%prog [options]
|
|
@@ -589,14 +613,14 @@ def main():
|
589
|
613
|
parser = optparse.OptionParser(usage=usage,
|
590
|
614
|
description=META_INFO['description'],
|
591
|
615
|
version='%%prog %s' % META_INFO['version'])
|
592
|
|
- parser.add_option('-c', '--color', default='auto', metavar='WHEN',
|
593
|
|
- help='colorize mode "auto" (default), "always", or "never"')
|
594
|
616
|
parser.add_option('-s', '--side-by-side', action='store_true',
|
595
|
617
|
help='show in side-by-side mode')
|
596
|
618
|
parser.add_option('-w', '--width', type='int', default=80, metavar='N',
|
597
|
619
|
help='set text width (side-by-side mode only), default is 80')
|
598
|
620
|
parser.add_option('-l', '--log', action='store_true',
|
599
|
621
|
help='show diff log from revision control')
|
|
622
|
+ parser.add_option('-c', '--color', default='auto', metavar='X',
|
|
623
|
+ help='colorize mode "auto" (default), "always", or "never"')
|
600
|
624
|
opts, args = parser.parse_args()
|
601
|
625
|
|
602
|
626
|
if opts.log:
|