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