~abentley/bzrtools/bzrtools.dev

399 by Aaron Bentley
Implement cdiff (based on old Fai code)
1
# Copyright (C) 2006 Aaron Bentley
2
# <aaron.bentley@utoronto.ca>
3
#
4
#    This program is free software; you can redistribute it and/or modify
5
#    it under the terms of the GNU General Public License as published by
6
#    the Free Software Foundation; either version 2 of the License, or
7
#    (at your option) any later version.
8
#
9
#    This program is distributed in the hope that it will be useful,
10
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
#    GNU General Public License for more details.
13
#
14
#    You should have received a copy of the GNU General Public License
15
#    along with this program; if not, write to the Free Software
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18
import sys
400.1.2 by Michael Ellerman
Parse ~/.colordiffrc for colour information if it's there.
19
from os.path import expanduser
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
20
497 by Aaron Bentley
Add optional style checks to colordiff
21
from bzrlib import patiencediff, trace
408.1.1 by Adeodato Simó
Get command objects via get_cmd_object(), instead of directly importing
22
from bzrlib.commands import get_cmd_object
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
23
from bzrlib.patches import (hunk_from_header, InsertLine, RemoveLine,
24
                            ContextLine, Hunk)
25
400.1.2 by Michael Ellerman
Parse ~/.colordiffrc for colour information if it's there.
26
import terminal
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
27
28
class LineParser(object):
29
    def parse_line(self, line):
30
        if line.startswith("@"):
31
            return hunk_from_header(line)
32
        elif line.startswith("+"):
33
            return InsertLine(line[1:])
34
        elif line.startswith("-"):
35
            return RemoveLine(line[1:])
36
        elif line.startswith(" "):
37
            return ContextLine(line[1:])
38
        else:
39
            return line
40
400.1.2 by Michael Ellerman
Parse ~/.colordiffrc for colour information if it's there.
41
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
42
class DiffWriter(object):
400.1.2 by Michael Ellerman
Parse ~/.colordiffrc for colour information if it's there.
43
44
    colors = {
406 by Aaron Bentley
Rename modline -> metaline, style tweakage
45
        'metaline':    'darkyellow',
412 by Aaron Bentley
Make Ellerman's cdiff colours the default
46
        'plain':       'darkwhite',
47
        'newtext':     'darkblue',
48
        'oldtext':     'darkred',
49
        'diffstuff':   'darkgreen'
400.1.2 by Michael Ellerman
Parse ~/.colordiffrc for colour information if it's there.
50
    }
51
497 by Aaron Bentley
Add optional style checks to colordiff
52
    def __init__(self, target, check_style):
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
53
        self.target = target
54
        self.lp = LineParser()
400 by Aaron Bentley
Don't just assume we get lines
55
        self.chunks = []
400.1.2 by Michael Ellerman
Parse ~/.colordiffrc for colour information if it's there.
56
        self._read_colordiffrc()
497 by Aaron Bentley
Add optional style checks to colordiff
57
        self.added_trailing_whitespace = 0
58
        self.spurious_whitespace = 0
506 by Aaron Bentley
Colordiff warns on long lines
59
        self.long_lines = 0
60
        self.max_line_len = 79 
497 by Aaron Bentley
Add optional style checks to colordiff
61
        self._new_lines = []
62
        self._old_lines = []
63
        self.check_style = check_style
400.1.2 by Michael Ellerman
Parse ~/.colordiffrc for colour information if it's there.
64
65
    def _read_colordiffrc(self):
66
        path = expanduser('~/.colordiffrc')
67
        try:
68
            f = open(path, 'r')
69
        except IOError:
70
            return
71
72
        for line in f.readlines():
73
            try:
74
                key, val = line.split('=')
75
            except ValueError:
76
                continue
77
78
            key = key.strip()
79
            val = val.strip()
80
81
            tmp = val
82
            if val.startswith('dark'):
83
                tmp = val[4:]
84
85
            if tmp not in terminal.colors:
86
                continue
87
88
            self.colors[key] = val
89
90
    def colorstring(self, type, string):
91
        color = self.colors[type]
400.1.3 by Michael Ellerman
Support for colordiff's 'plain', which colours the context text.
92
        if color is not None:
93
            string = terminal.colorstring(str(string), color)
94
        self.target.write(string)
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
95
96
    def write(self, text):
400 by Aaron Bentley
Don't just assume we get lines
97
        newstuff = text.split('\n')
98
        for newchunk in newstuff[:-1]:
99
            self._writeline(''.join(self.chunks + [newchunk, '\n']))
100
            self.chunks = []
101
        self.chunks = [newstuff[-1]]
102
103
    def _writeline(self, line):
104
        item = self.lp.parse_line(line)
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
105
        if isinstance(item, Hunk):
406 by Aaron Bentley
Rename modline -> metaline, style tweakage
106
            line_class = 'diffstuff'
497 by Aaron Bentley
Add optional style checks to colordiff
107
            self._analyse_old_new()
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
108
        elif isinstance(item, InsertLine):
503 by Aaron Bentley
Update from review comments
109
            if item.contents.endswith(' \n'):
497 by Aaron Bentley
Add optional style checks to colordiff
110
                self.added_trailing_whitespace += 1
507 by Aaron Bentley
Fix long line detection
111
            if len(item.contents.rstrip('\n')) > self.max_line_len:
506 by Aaron Bentley
Colordiff warns on long lines
112
                self.long_lines += 1
406 by Aaron Bentley
Rename modline -> metaline, style tweakage
113
            line_class = 'newtext'
503 by Aaron Bentley
Update from review comments
114
            self._new_lines.append(item)
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
115
        elif isinstance(item, RemoveLine):
406 by Aaron Bentley
Rename modline -> metaline, style tweakage
116
            line_class = 'oldtext'
503 by Aaron Bentley
Update from review comments
117
            self._old_lines.append(item)
400.1.3 by Michael Ellerman
Support for colordiff's 'plain', which colours the context text.
118
        elif isinstance(item, basestring) and item.startswith('==='):
406 by Aaron Bentley
Rename modline -> metaline, style tweakage
119
            line_class = 'metaline'
497 by Aaron Bentley
Add optional style checks to colordiff
120
            self._analyse_old_new()
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
121
        else:
406 by Aaron Bentley
Rename modline -> metaline, style tweakage
122
            line_class = 'plain'
497 by Aaron Bentley
Add optional style checks to colordiff
123
            self._analyse_old_new()
406 by Aaron Bentley
Rename modline -> metaline, style tweakage
124
        self.colorstring(line_class, str(item))
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
125
126
    def flush(self):
127
        self.target.flush()
128
497 by Aaron Bentley
Add optional style checks to colordiff
129
    @staticmethod
130
    def _matched_lines(old, new):
131
        matcher = patiencediff.PatienceSequenceMatcher(None, old, new)
132
        matched_lines = sum (n for i, j, n in matcher.get_matching_blocks())
133
        return matched_lines
134
135
    def _analyse_old_new(self):
136
        if (self._old_lines, self._new_lines) == ([], []):
137
            return
138
        if not self.check_style:
139
            return
503 by Aaron Bentley
Update from review comments
140
        old = [l.contents for l in self._old_lines]
141
        new = [l.contents for l in self._new_lines]
142
        ws_matched = self._matched_lines(old, new)
143
        old = [l.rstrip() for l in old]
144
        new = [l.rstrip() for l in new]
497 by Aaron Bentley
Add optional style checks to colordiff
145
        no_ws_matched = self._matched_lines(old, new)
146
        assert no_ws_matched >= ws_matched
147
        if no_ws_matched > ws_matched:
148
            self.spurious_whitespace += no_ws_matched - ws_matched
149
            self.target.write('^ Spurious whitespace change above.\n')
150
        self._old_lines, self._new_lines = ([], [])
151
152
153
def colordiff(check_style, *args, **kwargs):
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
154
    real_stdout = sys.stdout
497 by Aaron Bentley
Add optional style checks to colordiff
155
    dw = DiffWriter(real_stdout, check_style)
156
    sys.stdout = dw
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
157
    try:
408.1.1 by Adeodato Simó
Get command objects via get_cmd_object(), instead of directly importing
158
        get_cmd_object('diff').run(*args, **kwargs)
399 by Aaron Bentley
Implement cdiff (based on old Fai code)
159
    finally:
160
        sys.stdout = real_stdout
497 by Aaron Bentley
Add optional style checks to colordiff
161
    if check_style:
162
        if dw.added_trailing_whitespace > 0:
503 by Aaron Bentley
Update from review comments
163
            trace.warning('%d new line(s) have trailing whitespace.' %
497 by Aaron Bentley
Add optional style checks to colordiff
164
                          dw.added_trailing_whitespace)
506 by Aaron Bentley
Colordiff warns on long lines
165
        if dw.long_lines > 0:
166
            trace.warning('%d new line(s) exceed(s) %d columns.' %
167
                          (dw.long_lines, dw.max_line_len))
497 by Aaron Bentley
Add optional style checks to colordiff
168
        if dw.spurious_whitespace > 0:
503 by Aaron Bentley
Update from review comments
169
            trace.warning('%d line(s) have spurious whitespace changes' %
497 by Aaron Bentley
Add optional style checks to colordiff
170
                          dw.spurious_whitespace)