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
1=== modified file 'breezy/builtins.py'
2--- breezy/builtins.py 2020-01-30 19:30:52 +0000
3+++ breezy/builtins.py 2020-01-31 03:06:15 +0000
4@@ -2335,14 +2335,27 @@
5 help='How many lines of context to show.',
6 type=int,
7 ),
8+ RegistryOption.from_kwargs(
9+ 'color',
10+ help='Color mode to use.',
11+ title='Color Mode', value_switches=False, enum_switch=True,
12+ never='Never colorize output.',
13+ auto='Only colorize output if terminal supports it and STDOUT is a'
14+ ' TTY.',
15+ always='Always colorize output (default).'),
16+ Option(
17+ 'check-style',
18+ help=('Warn if trailing whitespace or spurious changes have been'
19+ ' added.'))
20 ]
21+
22 aliases = ['di', 'dif']
23 encoding_type = 'exact'
24
25 @display_command
26 def run(self, revision=None, file_list=None, diff_options=None,
27 prefix=None, old=None, new=None, using=None, format=None,
28- context=None):
29+ context=None, color='never'):
30 from .diff import (get_trees_and_branches_to_diff_locked,
31 show_diff_trees)
32
33@@ -2375,7 +2388,17 @@
34 file_list, revision, old, new, self._exit_stack, apply_view=True)
35 # GNU diff on Windows uses ANSI encoding for filenames
36 path_encoding = osutils.get_diff_header_encoding()
37- return show_diff_trees(old_tree, new_tree, self.outf,
38+ outf = self.outf
39+ if color == 'auto':
40+ from .terminal import has_ansi_colors
41+ if has_ansi_colors():
42+ color = 'always'
43+ else:
44+ color = 'never'
45+ if 'always' == color:
46+ from .colordiff import DiffWriter
47+ outf = DiffWriter(outf)
48+ return show_diff_trees(old_tree, new_tree, outf,
49 specific_files=specific_files,
50 external_diff_options=diff_options,
51 old_label=old_label, new_label=new_label,
52
53=== added file 'breezy/colordiff.py'
54--- breezy/colordiff.py 1970-01-01 00:00:00 +0000
55+++ breezy/colordiff.py 2020-01-31 03:06:15 +0000
56@@ -0,0 +1,197 @@
57+# Copyright (C) 2006 Aaron Bentley
58+# <aaron@aaronbentley.com>
59+#
60+# This program is free software; you can redistribute it and/or modify
61+# it under the terms of the GNU General Public License as published by
62+# the Free Software Foundation; either version 2 of the License, or
63+# (at your option) any later version.
64+#
65+# This program is distributed in the hope that it will be useful,
66+# but WITHOUT ANY WARRANTY; without even the implied warranty of
67+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
68+# GNU General Public License for more details.
69+#
70+# You should have received a copy of the GNU General Public License
71+# along with this program; if not, write to the Free Software
72+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
73+
74+from __future__ import absolute_import
75+
76+import re
77+import sys
78+from os.path import expanduser
79+import patiencediff
80+
81+from . import trace, terminal
82+from .commands import get_cmd_object
83+from .patches import (hunk_from_header, InsertLine, RemoveLine,
84+ ContextLine, Hunk, HunkLine)
85+
86+
87+GLOBAL_COLORDIFFRC = '/etc/colordiffrc'
88+
89+class LineParser(object):
90+
91+ def parse_line(self, line):
92+ if line.startswith(b"@"):
93+ return hunk_from_header(line)
94+ elif line.startswith(b"+"):
95+ return InsertLine(line[1:])
96+ elif line.startswith(b"-"):
97+ return RemoveLine(line[1:])
98+ elif line.startswith(b" "):
99+ return ContextLine(line[1:])
100+ else:
101+ return line
102+
103+
104+def read_colordiffrc(path):
105+ colors = {}
106+ with open(path, 'r') as f:
107+ for line in f.readlines():
108+ try:
109+ key, val = line.split('=')
110+ except ValueError:
111+ continue
112+
113+ key = key.strip()
114+ val = val.strip()
115+
116+ tmp = val
117+ if val.startswith('dark'):
118+ tmp = val[4:]
119+
120+ if tmp not in terminal.colors:
121+ continue
122+
123+ colors[key] = val
124+ return colors
125+
126+
127+class DiffWriter(object):
128+
129+ def __init__(self, target, check_style=False):
130+ self.target = target
131+ self.lp = LineParser()
132+ self.chunks = []
133+ self.colors = {
134+ 'metaline': 'darkyellow',
135+ 'plain': 'darkwhite',
136+ 'newtext': 'darkblue',
137+ 'oldtext': 'darkred',
138+ 'diffstuff': 'darkgreen',
139+ 'trailingspace': 'yellow',
140+ 'leadingtabs': 'magenta',
141+ 'longline': 'cyan',
142+ }
143+ if GLOBAL_COLORDIFFRC is not None:
144+ self._read_colordiffrc(GLOBAL_COLORDIFFRC)
145+ self._read_colordiffrc(expanduser('~/.colordiffrc'))
146+ self.added_leading_tabs = 0
147+ self.added_trailing_whitespace = 0
148+ self.spurious_whitespace = 0
149+ self.long_lines = 0
150+ self.max_line_len = 79
151+ self._new_lines = []
152+ self._old_lines = []
153+ self.check_style = check_style
154+
155+ def _read_colordiffrc(self, path):
156+ try:
157+ self.colors.update(read_colordiffrc(path))
158+ except IOError:
159+ pass
160+
161+ def colorstring(self, type, item, bad_ws_match):
162+ color = self.colors[type]
163+ if color is not None:
164+ if self.check_style and bad_ws_match:
165+ #highlight were needed
166+ item.contents = ''.join(
167+ terminal.colorstring(txt, color, bcol)
168+ for txt, bcol in (
169+ (bad_ws_match.group(1).expandtabs(),
170+ self.colors['leadingtabs']),
171+ (bad_ws_match.group(2)[0:self.max_line_len], None),
172+ (bad_ws_match.group(2)[self.max_line_len:],
173+ self.colors['longline']),
174+ (bad_ws_match.group(3), self.colors['trailingspace'])
175+ )) + bad_ws_match.group(4)
176+ if not isinstance(item, bytes):
177+ item = item.as_bytes()
178+ string = terminal.colorstring(item, color)
179+ else:
180+ string = str(item)
181+ self.target.write(string)
182+
183+ def write(self, text):
184+ newstuff = text.split(b'\n')
185+ for newchunk in newstuff[:-1]:
186+ self._writeline(b''.join(self.chunks + [newchunk, b'\n']))
187+ self.chunks = []
188+ self.chunks = [newstuff[-1]]
189+
190+ def writelines(self, lines):
191+ for line in lines:
192+ self.write(line)
193+
194+ def _writeline(self, line):
195+ item = self.lp.parse_line(line)
196+ bad_ws_match = None
197+ if isinstance(item, Hunk):
198+ line_class = 'diffstuff'
199+ self._analyse_old_new()
200+ elif isinstance(item, HunkLine):
201+ bad_ws_match = re.match(br'^([\t]*)(.*?)([\t ]*)(\r?\n)$',
202+ item.contents)
203+ has_leading_tabs = bool(bad_ws_match.group(1))
204+ has_trailing_whitespace = bool(bad_ws_match.group(3))
205+ if isinstance(item, InsertLine):
206+ if has_leading_tabs:
207+ self.added_leading_tabs += 1
208+ if has_trailing_whitespace:
209+ self.added_trailing_whitespace += 1
210+ if (len(bad_ws_match.group(2)) > self.max_line_len and
211+ not item.contents.startswith(b'++ ')):
212+ self.long_lines += 1
213+ line_class = 'newtext'
214+ self._new_lines.append(item)
215+ elif isinstance(item, RemoveLine):
216+ line_class = 'oldtext'
217+ self._old_lines.append(item)
218+ else:
219+ line_class = 'plain'
220+ elif isinstance(item, bytes) and item.startswith(b'==='):
221+ line_class = 'metaline'
222+ self._analyse_old_new()
223+ else:
224+ line_class = 'plain'
225+ self._analyse_old_new()
226+ self.colorstring(line_class, item, bad_ws_match)
227+
228+ def flush(self):
229+ self.target.flush()
230+
231+ @staticmethod
232+ def _matched_lines(old, new):
233+ matcher = patiencediff.PatienceSequenceMatcher(None, old, new)
234+ matched_lines = sum(n for i, j, n in matcher.get_matching_blocks())
235+ return matched_lines
236+
237+ def _analyse_old_new(self):
238+ if (self._old_lines, self._new_lines) == ([], []):
239+ return
240+ if not self.check_style:
241+ return
242+ old = [l.contents for l in self._old_lines]
243+ new = [l.contents for l in self._new_lines]
244+ ws_matched = self._matched_lines(old, new)
245+ old = [l.rstrip() for l in old]
246+ new = [l.rstrip() for l in new]
247+ no_ws_matched = self._matched_lines(old, new)
248+ if no_ws_matched < ws_matched:
249+ raise AssertionError
250+ if no_ws_matched > ws_matched:
251+ self.spurious_whitespace += no_ws_matched - ws_matched
252+ self.target.write('^ Spurious whitespace change above.\n')
253+ self._old_lines, self._new_lines = ([], [])
254
255=== added file 'breezy/terminal.py'
256--- breezy/terminal.py 1970-01-01 00:00:00 +0000
257+++ breezy/terminal.py 2020-01-31 03:06:15 +0000
258@@ -0,0 +1,87 @@
259+# Copyright (C) 2004 Aaron Bentley
260+# <aaron@aaronbentley.com>
261+#
262+# This program is free software; you can redistribute it and/or modify
263+# it under the terms of the GNU General Public License as published by
264+# the Free Software Foundation; either version 2 of the License, or
265+# (at your option) any later version.
266+#
267+# This program is distributed in the hope that it will be useful,
268+# but WITHOUT ANY WARRANTY; without even the implied warranty of
269+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
270+# GNU General Public License for more details.
271+#
272+# You should have received a copy of the GNU General Public License
273+# along with this program; if not, write to the Free Software
274+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
275+
276+from __future__ import absolute_import
277+
278+import os
279+import sys
280+
281+__docformat__ = "restructuredtext"
282+__doc__ = "Terminal control functionality"
283+
284+def has_ansi_colors():
285+ # XXX The whole color handling should be rewritten to use terminfo
286+ # XXX before we get there, checking for setaf capability should do.
287+ # XXX See terminfo(5) for all the gory details.
288+ if sys.platform == "win32":
289+ return False
290+ if not sys.stdout.isatty():
291+ return False
292+ import curses
293+ try:
294+ curses.setupterm()
295+ except curses.error:
296+ return False
297+ return bool(curses.tigetstr('setaf'))
298+
299+colors = {
300+ "black": b"0",
301+ "red": b"1",
302+ "green": b"2",
303+ "yellow": b"3",
304+ "blue": b"4",
305+ "magenta": b"5",
306+ "cyan": b"6",
307+ "white": b"7"
308+}
309+
310+def colorstring(text, fgcolor=None, bgcolor=None):
311+ """
312+ Returns a string using ANSI control codes to set the text color.
313+
314+ :param text: The text to set the color for.
315+ :type text: string
316+ :param fgcolor: The foreground color to use
317+ :type fgcolor: string
318+ :param bgcolor: The background color to use
319+ :type bgcolor: string
320+ """
321+ code = []
322+
323+ if fgcolor:
324+ if fgcolor.startswith('dark'):
325+ code.append(b'0')
326+ fgcolor = fgcolor[4:]
327+ else:
328+ code.append(b'1')
329+
330+ code.append(b'3' + colors[fgcolor])
331+
332+ if bgcolor:
333+ code.append(b'4' + colors[bgcolor])
334+
335+ return b"".join((b"\033[", b';'.join(code), b"m", text, b"\033[0m"))
336+
337+
338+def term_title(title):
339+ term = os.environ.get('TERM', '')
340+ if term.startswith('xterm') or term == 'dtterm':
341+ return "\033]0;%s\007" % title
342+ return str()
343+
344+
345+# arch-tag: a79b9993-146e-4a51-8bae-a13791703ddd
346
347=== modified file 'breezy/tests/blackbox/test_diff.py'
348--- breezy/tests/blackbox/test_diff.py 2019-07-07 18:59:49 +0000
349+++ breezy/tests/blackbox/test_diff.py 2020-01-31 03:06:15 +0000
350@@ -269,6 +269,28 @@
351 "+contents of branch1/file\n"
352 "\n", subst_dates(out))
353
354+ def test_diff_color_always(self):
355+ from ...terminal import colorstring
356+ from ... import colordiff
357+ self.overrideAttr(colordiff, 'GLOBAL_COLORDIFFRC', None)
358+ self.example_branches()
359+ branch2_tree = workingtree.WorkingTree.open_containing('branch2')[0]
360+ self.build_tree_contents([('branch2/file', b'even newer content')])
361+ branch2_tree.commit(message='update file once more')
362+
363+ out, err = self.run_bzr('diff --color=always -r revno:2:branch2..revno:1:branch1',
364+ retcode=1)
365+ self.assertEqual('', err)
366+ self.assertEqualDiff((
367+ colorstring(b"=== modified file 'file'\n", 'darkyellow') +
368+ colorstring(b"--- old/file\tYYYY-MM-DD HH:MM:SS +ZZZZ\n", 'darkred') +
369+ colorstring(b"+++ new/file\tYYYY-MM-DD HH:MM:SS +ZZZZ\n", 'darkblue') +
370+ colorstring(b"@@ -1 +1 @@\n", 'darkgreen') +
371+ colorstring(b"-new content\n", 'darkred') +
372+ colorstring(b"+contents of branch1/file\n", 'darkblue') +
373+ colorstring(b"\n", 'darkwhite')).decode(),
374+ subst_dates(out))
375+
376 def example_branch2(self):
377 branch1_tree = self.make_branch_and_tree('branch1')
378 self.build_tree_contents([('branch1/file1', b'original line\n')])
379
380=== modified file 'doc/en/release-notes/brz-3.1.txt'
381--- doc/en/release-notes/brz-3.1.txt 2020-01-30 17:54:42 +0000
382+++ doc/en/release-notes/brz-3.1.txt 2020-01-31 03:06:15 +0000
383@@ -46,6 +46,11 @@
384 have installed and speeds up import time since psutil brings in
385 various other modules. (Jelmer Vernooij)
386
387+ * ``brz diff`` now has a --color argument that can write
388+ color diff output. This is based on the cdiff code in
389+ bzrtools by Aaron Bentley.
390+ (Jelmer Vernooij, #376594)
391+
392 * Information about tree references can now be updated on remote
393 branches. (Jelmer Vernooij)
394

Subscribers

People subscribed via source and target branches