Merge lp:~jelmer/brz/colordiff into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/colordiff
Merge into: lp:brz
Diff against target: 393 lines (+336/-2)
5 files modified
breezy/builtins.py (+25/-2)
breezy/colordiff.py (+197/-0)
breezy/terminal.py (+87/-0)
breezy/tests/blackbox/test_diff.py (+22/-0)
doc/en/release-notes/brz-3.1.txt (+5/-0)
To merge this branch: bzr merge lp:~jelmer/brz/colordiff
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+374749@code.launchpad.net

Commit message

Add support for a --color={auto,never,always} argument to 'brz diff'.

Description of the change

Add support for a --color={auto,never,always} argument to 'brz diff'.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'breezy/builtins.py'
--- breezy/builtins.py 2020-01-30 19:30:52 +0000
+++ breezy/builtins.py 2020-01-31 03:06:15 +0000
@@ -2335,14 +2335,27 @@
2335 help='How many lines of context to show.',2335 help='How many lines of context to show.',
2336 type=int,2336 type=int,
2337 ),2337 ),
2338 RegistryOption.from_kwargs(
2339 'color',
2340 help='Color mode to use.',
2341 title='Color Mode', value_switches=False, enum_switch=True,
2342 never='Never colorize output.',
2343 auto='Only colorize output if terminal supports it and STDOUT is a'
2344 ' TTY.',
2345 always='Always colorize output (default).'),
2346 Option(
2347 'check-style',
2348 help=('Warn if trailing whitespace or spurious changes have been'
2349 ' added.'))
2338 ]2350 ]
2351
2339 aliases = ['di', 'dif']2352 aliases = ['di', 'dif']
2340 encoding_type = 'exact'2353 encoding_type = 'exact'
23412354
2342 @display_command2355 @display_command
2343 def run(self, revision=None, file_list=None, diff_options=None,2356 def run(self, revision=None, file_list=None, diff_options=None,
2344 prefix=None, old=None, new=None, using=None, format=None,2357 prefix=None, old=None, new=None, using=None, format=None,
2345 context=None):2358 context=None, color='never'):
2346 from .diff import (get_trees_and_branches_to_diff_locked,2359 from .diff import (get_trees_and_branches_to_diff_locked,
2347 show_diff_trees)2360 show_diff_trees)
23482361
@@ -2375,7 +2388,17 @@
2375 file_list, revision, old, new, self._exit_stack, apply_view=True)2388 file_list, revision, old, new, self._exit_stack, apply_view=True)
2376 # GNU diff on Windows uses ANSI encoding for filenames2389 # GNU diff on Windows uses ANSI encoding for filenames
2377 path_encoding = osutils.get_diff_header_encoding()2390 path_encoding = osutils.get_diff_header_encoding()
2378 return show_diff_trees(old_tree, new_tree, self.outf,2391 outf = self.outf
2392 if color == 'auto':
2393 from .terminal import has_ansi_colors
2394 if has_ansi_colors():
2395 color = 'always'
2396 else:
2397 color = 'never'
2398 if 'always' == color:
2399 from .colordiff import DiffWriter
2400 outf = DiffWriter(outf)
2401 return show_diff_trees(old_tree, new_tree, outf,
2379 specific_files=specific_files,2402 specific_files=specific_files,
2380 external_diff_options=diff_options,2403 external_diff_options=diff_options,
2381 old_label=old_label, new_label=new_label,2404 old_label=old_label, new_label=new_label,
23822405
=== added file 'breezy/colordiff.py'
--- breezy/colordiff.py 1970-01-01 00:00:00 +0000
+++ breezy/colordiff.py 2020-01-31 03:06:15 +0000
@@ -0,0 +1,197 @@
1# Copyright (C) 2006 Aaron Bentley
2# <aaron@aaronbentley.com>
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18from __future__ import absolute_import
19
20import re
21import sys
22from os.path import expanduser
23import patiencediff
24
25from . import trace, terminal
26from .commands import get_cmd_object
27from .patches import (hunk_from_header, InsertLine, RemoveLine,
28 ContextLine, Hunk, HunkLine)
29
30
31GLOBAL_COLORDIFFRC = '/etc/colordiffrc'
32
33class LineParser(object):
34
35 def parse_line(self, line):
36 if line.startswith(b"@"):
37 return hunk_from_header(line)
38 elif line.startswith(b"+"):
39 return InsertLine(line[1:])
40 elif line.startswith(b"-"):
41 return RemoveLine(line[1:])
42 elif line.startswith(b" "):
43 return ContextLine(line[1:])
44 else:
45 return line
46
47
48def read_colordiffrc(path):
49 colors = {}
50 with open(path, 'r') as f:
51 for line in f.readlines():
52 try:
53 key, val = line.split('=')
54 except ValueError:
55 continue
56
57 key = key.strip()
58 val = val.strip()
59
60 tmp = val
61 if val.startswith('dark'):
62 tmp = val[4:]
63
64 if tmp not in terminal.colors:
65 continue
66
67 colors[key] = val
68 return colors
69
70
71class DiffWriter(object):
72
73 def __init__(self, target, check_style=False):
74 self.target = target
75 self.lp = LineParser()
76 self.chunks = []
77 self.colors = {
78 'metaline': 'darkyellow',
79 'plain': 'darkwhite',
80 'newtext': 'darkblue',
81 'oldtext': 'darkred',
82 'diffstuff': 'darkgreen',
83 'trailingspace': 'yellow',
84 'leadingtabs': 'magenta',
85 'longline': 'cyan',
86 }
87 if GLOBAL_COLORDIFFRC is not None:
88 self._read_colordiffrc(GLOBAL_COLORDIFFRC)
89 self._read_colordiffrc(expanduser('~/.colordiffrc'))
90 self.added_leading_tabs = 0
91 self.added_trailing_whitespace = 0
92 self.spurious_whitespace = 0
93 self.long_lines = 0
94 self.max_line_len = 79
95 self._new_lines = []
96 self._old_lines = []
97 self.check_style = check_style
98
99 def _read_colordiffrc(self, path):
100 try:
101 self.colors.update(read_colordiffrc(path))
102 except IOError:
103 pass
104
105 def colorstring(self, type, item, bad_ws_match):
106 color = self.colors[type]
107 if color is not None:
108 if self.check_style and bad_ws_match:
109 #highlight were needed
110 item.contents = ''.join(
111 terminal.colorstring(txt, color, bcol)
112 for txt, bcol in (
113 (bad_ws_match.group(1).expandtabs(),
114 self.colors['leadingtabs']),
115 (bad_ws_match.group(2)[0:self.max_line_len], None),
116 (bad_ws_match.group(2)[self.max_line_len:],
117 self.colors['longline']),
118 (bad_ws_match.group(3), self.colors['trailingspace'])
119 )) + bad_ws_match.group(4)
120 if not isinstance(item, bytes):
121 item = item.as_bytes()
122 string = terminal.colorstring(item, color)
123 else:
124 string = str(item)
125 self.target.write(string)
126
127 def write(self, text):
128 newstuff = text.split(b'\n')
129 for newchunk in newstuff[:-1]:
130 self._writeline(b''.join(self.chunks + [newchunk, b'\n']))
131 self.chunks = []
132 self.chunks = [newstuff[-1]]
133
134 def writelines(self, lines):
135 for line in lines:
136 self.write(line)
137
138 def _writeline(self, line):
139 item = self.lp.parse_line(line)
140 bad_ws_match = None
141 if isinstance(item, Hunk):
142 line_class = 'diffstuff'
143 self._analyse_old_new()
144 elif isinstance(item, HunkLine):
145 bad_ws_match = re.match(br'^([\t]*)(.*?)([\t ]*)(\r?\n)$',
146 item.contents)
147 has_leading_tabs = bool(bad_ws_match.group(1))
148 has_trailing_whitespace = bool(bad_ws_match.group(3))
149 if isinstance(item, InsertLine):
150 if has_leading_tabs:
151 self.added_leading_tabs += 1
152 if has_trailing_whitespace:
153 self.added_trailing_whitespace += 1
154 if (len(bad_ws_match.group(2)) > self.max_line_len and
155 not item.contents.startswith(b'++ ')):
156 self.long_lines += 1
157 line_class = 'newtext'
158 self._new_lines.append(item)
159 elif isinstance(item, RemoveLine):
160 line_class = 'oldtext'
161 self._old_lines.append(item)
162 else:
163 line_class = 'plain'
164 elif isinstance(item, bytes) and item.startswith(b'==='):
165 line_class = 'metaline'
166 self._analyse_old_new()
167 else:
168 line_class = 'plain'
169 self._analyse_old_new()
170 self.colorstring(line_class, item, bad_ws_match)
171
172 def flush(self):
173 self.target.flush()
174
175 @staticmethod
176 def _matched_lines(old, new):
177 matcher = patiencediff.PatienceSequenceMatcher(None, old, new)
178 matched_lines = sum(n for i, j, n in matcher.get_matching_blocks())
179 return matched_lines
180
181 def _analyse_old_new(self):
182 if (self._old_lines, self._new_lines) == ([], []):
183 return
184 if not self.check_style:
185 return
186 old = [l.contents for l in self._old_lines]
187 new = [l.contents for l in self._new_lines]
188 ws_matched = self._matched_lines(old, new)
189 old = [l.rstrip() for l in old]
190 new = [l.rstrip() for l in new]
191 no_ws_matched = self._matched_lines(old, new)
192 if no_ws_matched < ws_matched:
193 raise AssertionError
194 if no_ws_matched > ws_matched:
195 self.spurious_whitespace += no_ws_matched - ws_matched
196 self.target.write('^ Spurious whitespace change above.\n')
197 self._old_lines, self._new_lines = ([], [])
0198
=== added file 'breezy/terminal.py'
--- breezy/terminal.py 1970-01-01 00:00:00 +0000
+++ breezy/terminal.py 2020-01-31 03:06:15 +0000
@@ -0,0 +1,87 @@
1# Copyright (C) 2004 Aaron Bentley
2# <aaron@aaronbentley.com>
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18from __future__ import absolute_import
19
20import os
21import sys
22
23__docformat__ = "restructuredtext"
24__doc__ = "Terminal control functionality"
25
26def has_ansi_colors():
27 # XXX The whole color handling should be rewritten to use terminfo
28 # XXX before we get there, checking for setaf capability should do.
29 # XXX See terminfo(5) for all the gory details.
30 if sys.platform == "win32":
31 return False
32 if not sys.stdout.isatty():
33 return False
34 import curses
35 try:
36 curses.setupterm()
37 except curses.error:
38 return False
39 return bool(curses.tigetstr('setaf'))
40
41colors = {
42 "black": b"0",
43 "red": b"1",
44 "green": b"2",
45 "yellow": b"3",
46 "blue": b"4",
47 "magenta": b"5",
48 "cyan": b"6",
49 "white": b"7"
50}
51
52def colorstring(text, fgcolor=None, bgcolor=None):
53 """
54 Returns a string using ANSI control codes to set the text color.
55
56 :param text: The text to set the color for.
57 :type text: string
58 :param fgcolor: The foreground color to use
59 :type fgcolor: string
60 :param bgcolor: The background color to use
61 :type bgcolor: string
62 """
63 code = []
64
65 if fgcolor:
66 if fgcolor.startswith('dark'):
67 code.append(b'0')
68 fgcolor = fgcolor[4:]
69 else:
70 code.append(b'1')
71
72 code.append(b'3' + colors[fgcolor])
73
74 if bgcolor:
75 code.append(b'4' + colors[bgcolor])
76
77 return b"".join((b"\033[", b';'.join(code), b"m", text, b"\033[0m"))
78
79
80def term_title(title):
81 term = os.environ.get('TERM', '')
82 if term.startswith('xterm') or term == 'dtterm':
83 return "\033]0;%s\007" % title
84 return str()
85
86
87# arch-tag: a79b9993-146e-4a51-8bae-a13791703ddd
088
=== modified file 'breezy/tests/blackbox/test_diff.py'
--- breezy/tests/blackbox/test_diff.py 2019-07-07 18:59:49 +0000
+++ breezy/tests/blackbox/test_diff.py 2020-01-31 03:06:15 +0000
@@ -269,6 +269,28 @@
269 "+contents of branch1/file\n"269 "+contents of branch1/file\n"
270 "\n", subst_dates(out))270 "\n", subst_dates(out))
271271
272 def test_diff_color_always(self):
273 from ...terminal import colorstring
274 from ... import colordiff
275 self.overrideAttr(colordiff, 'GLOBAL_COLORDIFFRC', None)
276 self.example_branches()
277 branch2_tree = workingtree.WorkingTree.open_containing('branch2')[0]
278 self.build_tree_contents([('branch2/file', b'even newer content')])
279 branch2_tree.commit(message='update file once more')
280
281 out, err = self.run_bzr('diff --color=always -r revno:2:branch2..revno:1:branch1',
282 retcode=1)
283 self.assertEqual('', err)
284 self.assertEqualDiff((
285 colorstring(b"=== modified file 'file'\n", 'darkyellow') +
286 colorstring(b"--- old/file\tYYYY-MM-DD HH:MM:SS +ZZZZ\n", 'darkred') +
287 colorstring(b"+++ new/file\tYYYY-MM-DD HH:MM:SS +ZZZZ\n", 'darkblue') +
288 colorstring(b"@@ -1 +1 @@\n", 'darkgreen') +
289 colorstring(b"-new content\n", 'darkred') +
290 colorstring(b"+contents of branch1/file\n", 'darkblue') +
291 colorstring(b"\n", 'darkwhite')).decode(),
292 subst_dates(out))
293
272 def example_branch2(self):294 def example_branch2(self):
273 branch1_tree = self.make_branch_and_tree('branch1')295 branch1_tree = self.make_branch_and_tree('branch1')
274 self.build_tree_contents([('branch1/file1', b'original line\n')])296 self.build_tree_contents([('branch1/file1', b'original line\n')])
275297
=== modified file 'doc/en/release-notes/brz-3.1.txt'
--- doc/en/release-notes/brz-3.1.txt 2020-01-30 17:54:42 +0000
+++ doc/en/release-notes/brz-3.1.txt 2020-01-31 03:06:15 +0000
@@ -46,6 +46,11 @@
46 have installed and speeds up import time since psutil brings in46 have installed and speeds up import time since psutil brings in
47 various other modules. (Jelmer Vernooij)47 various other modules. (Jelmer Vernooij)
4848
49 * ``brz diff`` now has a --color argument that can write
50 color diff output. This is based on the cdiff code in
51 bzrtools by Aaron Bentley.
52 (Jelmer Vernooij, #376594)
53
49 * Information about tree references can now be updated on remote54 * Information about tree references can now be updated on remote
50 branches. (Jelmer Vernooij)55 branches. (Jelmer Vernooij)
5156

Subscribers

People subscribed via source and target branches