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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij | Approve | ||
Review via email: mp+374749@code.launchpad.net |
Commit message
Add support for a --color=
Description of the change
Add support for a --color=
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 : | # |
Running landing tests failed
https:/
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 |
Running landing tests failed /ci.breezy- vcs.org/ job/brz/ job/brz- land/678/
https:/