##// END OF EJS Templates
Merge with default
Martin Geisler -
r10477:44b4a2a3 merge stable
parent child Browse files
Show More
@@ -1,368 +1,374 b''
1 1 # color.py color output for the status and qseries commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 4 #
5 5 # This program is free software; you can redistribute it and/or modify it
6 6 # under the terms of the GNU General Public License as published by the
7 7 # Free Software Foundation; either version 2 of the License, or (at your
8 8 # option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful, but
11 11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 13 # Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License along
16 16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 18
19 19 '''colorize output from some commands
20 20
21 21 This extension modifies the status and resolve commands to add color to their
22 22 output to reflect file status, the qseries command to add color to reflect
23 23 patch status (applied, unapplied, missing), and to diff-related
24 24 commands to highlight additions, removals, diff headers, and trailing
25 25 whitespace.
26 26
27 27 Other effects in addition to color, like bold and underlined text, are
28 28 also available. Effects are rendered with the ECMA-48 SGR control
29 29 function (aka ANSI escape codes). This module also provides the
30 30 render_text function, which can be used to add effects to any text.
31 31
32 32 Default effects may be overridden from the .hgrc file::
33 33
34 34 [color]
35 35 status.modified = blue bold underline red_background
36 36 status.added = green bold
37 37 status.removed = red bold blue_background
38 38 status.deleted = cyan bold underline
39 39 status.unknown = magenta bold underline
40 40 status.ignored = black bold
41 41
42 42 # 'none' turns off all effects
43 43 status.clean = none
44 44 status.copied = none
45 45
46 46 qseries.applied = blue bold underline
47 47 qseries.unapplied = black bold
48 48 qseries.missing = red bold
49 49
50 50 diff.diffline = bold
51 51 diff.extended = cyan bold
52 52 diff.file_a = red bold
53 53 diff.file_b = green bold
54 54 diff.hunk = magenta
55 55 diff.deleted = red
56 56 diff.inserted = green
57 57 diff.changed = white
58 58 diff.trailingwhitespace = bold red_background
59 59
60 60 resolve.unresolved = red bold
61 61 resolve.resolved = green bold
62 62
63 63 bookmarks.current = green
64 64 '''
65 65
66 66 import os, sys
67 67
68 68 from mercurial import cmdutil, commands, extensions
69 69 from mercurial.i18n import _
70 70
71 71 # start and stop parameters for effects
72 72 _effect_params = {'none': 0,
73 73 'black': 30,
74 74 'red': 31,
75 75 'green': 32,
76 76 'yellow': 33,
77 77 'blue': 34,
78 78 'magenta': 35,
79 79 'cyan': 36,
80 80 'white': 37,
81 81 'bold': 1,
82 82 'italic': 3,
83 83 'underline': 4,
84 84 'inverse': 7,
85 85 'black_background': 40,
86 86 'red_background': 41,
87 87 'green_background': 42,
88 88 'yellow_background': 43,
89 89 'blue_background': 44,
90 90 'purple_background': 45,
91 91 'cyan_background': 46,
92 92 'white_background': 47}
93 93
94 94 def render_effects(text, effects):
95 95 'Wrap text in commands to turn on each effect.'
96 96 start = [str(_effect_params[e]) for e in ['none'] + effects]
97 97 start = '\033[' + ';'.join(start) + 'm'
98 98 stop = '\033[' + str(_effect_params['none']) + 'm'
99 99 return ''.join([start, text, stop])
100 100
101 101 def _colorstatuslike(abbreviations, effectdefs, orig, ui, repo, *pats, **opts):
102 102 '''run a status-like command with colorized output'''
103 103 delimiter = opts.get('print0') and '\0' or '\n'
104 104
105 105 nostatus = opts.get('no_status')
106 106 opts['no_status'] = False
107 107 # run original command and capture its output
108 108 ui.pushbuffer()
109 109 retval = orig(ui, repo, *pats, **opts)
110 110 # filter out empty strings
111 111 lines_with_status = [line for line in ui.popbuffer().split(delimiter) if line]
112 112
113 113 if nostatus:
114 114 lines = [l[2:] for l in lines_with_status]
115 115 else:
116 116 lines = lines_with_status
117 117
118 118 # apply color to output and display it
119 119 for i in xrange(len(lines)):
120 status = abbreviations[lines_with_status[i][0]]
121 effects = effectdefs[status]
122 if effects:
123 lines[i] = render_effects(lines[i], effects)
120 try:
121 status = abbreviations[lines_with_status[i][0]]
122 except KeyError:
123 # Ignore lines with invalid codes, especially in the case of
124 # of unknown filenames containing newlines (issue2036).
125 pass
126 else:
127 effects = effectdefs[status]
128 if effects:
129 lines[i] = render_effects(lines[i], effects)
124 130 ui.write(lines[i] + delimiter)
125 131 return retval
126 132
127 133
128 134 _status_abbreviations = { 'M': 'modified',
129 135 'A': 'added',
130 136 'R': 'removed',
131 137 '!': 'deleted',
132 138 '?': 'unknown',
133 139 'I': 'ignored',
134 140 'C': 'clean',
135 141 ' ': 'copied', }
136 142
137 143 _status_effects = { 'modified': ['blue', 'bold'],
138 144 'added': ['green', 'bold'],
139 145 'removed': ['red', 'bold'],
140 146 'deleted': ['cyan', 'bold', 'underline'],
141 147 'unknown': ['magenta', 'bold', 'underline'],
142 148 'ignored': ['black', 'bold'],
143 149 'clean': ['none'],
144 150 'copied': ['none'], }
145 151
146 152 def colorstatus(orig, ui, repo, *pats, **opts):
147 153 '''run the status command with colored output'''
148 154 return _colorstatuslike(_status_abbreviations, _status_effects,
149 155 orig, ui, repo, *pats, **opts)
150 156
151 157
152 158 _resolve_abbreviations = { 'U': 'unresolved',
153 159 'R': 'resolved', }
154 160
155 161 _resolve_effects = { 'unresolved': ['red', 'bold'],
156 162 'resolved': ['green', 'bold'], }
157 163
158 164 def colorresolve(orig, ui, repo, *pats, **opts):
159 165 '''run the resolve command with colored output'''
160 166 if not opts.get('list'):
161 167 # only colorize for resolve -l
162 168 return orig(ui, repo, *pats, **opts)
163 169 return _colorstatuslike(_resolve_abbreviations, _resolve_effects,
164 170 orig, ui, repo, *pats, **opts)
165 171
166 172
167 173 _bookmark_effects = { 'current': ['green'] }
168 174
169 175 def colorbookmarks(orig, ui, repo, *pats, **opts):
170 176 def colorize(orig, s):
171 177 lines = s.split('\n')
172 178 for i, line in enumerate(lines):
173 179 if line.startswith(" *"):
174 180 lines[i] = render_effects(line, _bookmark_effects['current'])
175 181 orig('\n'.join(lines))
176 182 oldwrite = extensions.wrapfunction(ui, 'write', colorize)
177 183 try:
178 184 orig(ui, repo, *pats, **opts)
179 185 finally:
180 186 ui.write = oldwrite
181 187
182 188 def colorqseries(orig, ui, repo, *dummy, **opts):
183 189 '''run the qseries command with colored output'''
184 190 ui.pushbuffer()
185 191 retval = orig(ui, repo, **opts)
186 192 patchlines = ui.popbuffer().splitlines()
187 193 patchnames = repo.mq.series
188 194
189 195 for patch, patchname in zip(patchlines, patchnames):
190 196 if opts['missing']:
191 197 effects = _patch_effects['missing']
192 198 # Determine if patch is applied.
193 199 elif [applied for applied in repo.mq.applied
194 200 if patchname == applied.name]:
195 201 effects = _patch_effects['applied']
196 202 else:
197 203 effects = _patch_effects['unapplied']
198 204
199 205 patch = patch.replace(patchname, render_effects(patchname, effects), 1)
200 206 ui.write(patch + '\n')
201 207 return retval
202 208
203 209 _patch_effects = { 'applied': ['blue', 'bold', 'underline'],
204 210 'missing': ['red', 'bold'],
205 211 'unapplied': ['black', 'bold'], }
206 212 def colorwrap(orig, *args):
207 213 '''wrap ui.write for colored diff output'''
208 214 def _colorize(s):
209 215 lines = s.split('\n')
210 216 for i, line in enumerate(lines):
211 217 stripline = line
212 218 if line and line[0] in '+-':
213 219 # highlight trailing whitespace, but only in changed lines
214 220 stripline = line.rstrip()
215 221 for prefix, style in _diff_prefixes:
216 222 if stripline.startswith(prefix):
217 223 lines[i] = render_effects(stripline, _diff_effects[style])
218 224 break
219 225 if line != stripline:
220 226 lines[i] += render_effects(
221 227 line[len(stripline):], _diff_effects['trailingwhitespace'])
222 228 return '\n'.join(lines)
223 229 orig(*[_colorize(s) for s in args])
224 230
225 231 def colorshowpatch(orig, self, node):
226 232 '''wrap cmdutil.changeset_printer.showpatch with colored output'''
227 233 oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
228 234 try:
229 235 orig(self, node)
230 236 finally:
231 237 self.ui.write = oldwrite
232 238
233 239 def colordiffstat(orig, s):
234 240 lines = s.split('\n')
235 241 for i, line in enumerate(lines):
236 242 if line and line[-1] in '+-':
237 243 name, graph = line.rsplit(' ', 1)
238 244 graph = graph.replace('-',
239 245 render_effects('-', _diff_effects['deleted']))
240 246 graph = graph.replace('+',
241 247 render_effects('+', _diff_effects['inserted']))
242 248 lines[i] = ' '.join([name, graph])
243 249 orig('\n'.join(lines))
244 250
245 251 def colordiff(orig, ui, repo, *pats, **opts):
246 252 '''run the diff command with colored output'''
247 253 if opts.get('stat'):
248 254 wrapper = colordiffstat
249 255 else:
250 256 wrapper = colorwrap
251 257 oldwrite = extensions.wrapfunction(ui, 'write', wrapper)
252 258 try:
253 259 orig(ui, repo, *pats, **opts)
254 260 finally:
255 261 ui.write = oldwrite
256 262
257 263 def colorchurn(orig, ui, repo, *pats, **opts):
258 264 '''run the churn command with colored output'''
259 265 if not opts.get('diffstat'):
260 266 return orig(ui, repo, *pats, **opts)
261 267 oldwrite = extensions.wrapfunction(ui, 'write', colordiffstat)
262 268 try:
263 269 orig(ui, repo, *pats, **opts)
264 270 finally:
265 271 ui.write = oldwrite
266 272
267 273 _diff_prefixes = [('diff', 'diffline'),
268 274 ('copy', 'extended'),
269 275 ('rename', 'extended'),
270 276 ('old', 'extended'),
271 277 ('new', 'extended'),
272 278 ('deleted', 'extended'),
273 279 ('---', 'file_a'),
274 280 ('+++', 'file_b'),
275 281 ('@', 'hunk'),
276 282 ('-', 'deleted'),
277 283 ('+', 'inserted')]
278 284
279 285 _diff_effects = {'diffline': ['bold'],
280 286 'extended': ['cyan', 'bold'],
281 287 'file_a': ['red', 'bold'],
282 288 'file_b': ['green', 'bold'],
283 289 'hunk': ['magenta'],
284 290 'deleted': ['red'],
285 291 'inserted': ['green'],
286 292 'changed': ['white'],
287 293 'trailingwhitespace': ['bold', 'red_background']}
288 294
289 295 def extsetup(ui):
290 296 '''Initialize the extension.'''
291 297 _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
292 298 _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
293 299 _setupcmd(ui, 'log', commands.table, None, _diff_effects)
294 300 _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
295 301 _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
296 302 _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
297 303 _setupcmd(ui, 'resolve', commands.table, colorresolve, _resolve_effects)
298 304
299 305 try:
300 306 mq = extensions.find('mq')
301 307 _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
302 308 _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
303 309 except KeyError:
304 310 mq = None
305 311
306 312 try:
307 313 rec = extensions.find('record')
308 314 _setupcmd(ui, 'record', rec.cmdtable, colordiff, _diff_effects)
309 315 except KeyError:
310 316 rec = None
311 317
312 318 if mq and rec:
313 319 _setupcmd(ui, 'qrecord', rec.cmdtable, colordiff, _diff_effects)
314 320 try:
315 321 churn = extensions.find('churn')
316 322 _setupcmd(ui, 'churn', churn.cmdtable, colorchurn, _diff_effects)
317 323 except KeyError:
318 324 churn = None
319 325
320 326 try:
321 327 bookmarks = extensions.find('bookmarks')
322 328 _setupcmd(ui, 'bookmarks', bookmarks.cmdtable, colorbookmarks,
323 329 _bookmark_effects)
324 330 except KeyError:
325 331 # The bookmarks extension is not enabled
326 332 pass
327 333
328 334 def _setupcmd(ui, cmd, table, func, effectsmap):
329 335 '''patch in command to command table and load effect map'''
330 336 def nocolor(orig, *args, **opts):
331 337
332 338 if (opts['no_color'] or opts['color'] == 'never' or
333 339 (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
334 340 or not sys.__stdout__.isatty()))):
335 341 del opts['no_color']
336 342 del opts['color']
337 343 return orig(*args, **opts)
338 344
339 345 oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
340 346 'showpatch', colorshowpatch)
341 347 del opts['no_color']
342 348 del opts['color']
343 349 try:
344 350 if func is not None:
345 351 return func(orig, *args, **opts)
346 352 return orig(*args, **opts)
347 353 finally:
348 354 cmdutil.changeset_printer.showpatch = oldshowpatch
349 355
350 356 entry = extensions.wrapcommand(table, cmd, nocolor)
351 357 entry[1].extend([
352 358 ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
353 359 ('', 'no-color', None, _("don't colorize output (DEPRECATED)")),
354 360 ])
355 361
356 362 for status in effectsmap:
357 363 configkey = cmd + '.' + status
358 364 effects = ui.configlist('color', configkey)
359 365 if effects:
360 366 good = []
361 367 for e in effects:
362 368 if e in _effect_params:
363 369 good.append(e)
364 370 else:
365 371 ui.warn(_("ignoring unknown color/effect %r "
366 372 "(configured in color.%s)\n")
367 373 % (e, configkey))
368 374 effectsmap[status] = good
@@ -1,24 +1,43 b''
1 1 #!/bin/sh
2 2 # http://mercurial.selenic.com/bts/issue352
3 3
4 4 "$TESTDIR/hghave" eol-in-paths || exit 80
5 5
6 echo % test issue352
6 7 hg init foo
7 8 cd foo
8 9
9 10 A=`printf 'he\rllo'`
10 11
11 12 echo foo > "$A"
12 13 hg add
13 14 hg ci -A -m m
14 15 rm "$A"
15 16
16 17 echo foo > "hell
17 18 o"
18 19 hg add
19 20 hg ci -A -m m
20 21
21 22 echo foo > "$A"
22 23 hg debugwalk
23 24
25 # http://mercurial.selenic.com/bts/issue2036
26 cd ..
27 echo % test issue2039
28
29 hg init bar
30 cd bar
31
32 echo "[extensions]" >> $HGRCPATH
33 echo "color=" >> $HGRCPATH
34
35 A=`printf 'foo\nbar'`
36 B=`printf 'foo\nbar.baz'`
37
38 touch "$A"
39 touch "$B"
40
41 hg status --color=always
42
24 43 exit 0
@@ -1,14 +1,20 b''
1 % test issue352
1 2 adding he llo
2 3 abort: '\n' and '\r' disallowed in filenames: 'he\rllo'
3 4 adding he llo
4 5 abort: '\n' and '\r' disallowed in filenames: 'he\rllo'
5 6 adding hell
6 7 o
7 8 abort: '\n' and '\r' disallowed in filenames: 'hell\no'
8 9 adding hell
9 10 o
10 11 abort: '\n' and '\r' disallowed in filenames: 'hell\no'
11 12 f he llo he llo
12 13 f hell
13 14 o hell
14 15 o
16 % test issue2039
17 ? foo
18 bar
19 ? foo
20 bar.baz
General Comments 0
You need to be logged in to leave comments. Login now