##// END OF EJS Templates
remove unused imports
timeless -
r14139:4e5a36ee default
parent child Browse files
Show More
@@ -1,482 +1,482
1 # color.py color output for the status and qseries commands
1 # color.py color output for the status and qseries commands
2 #
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 #
4 #
5 # This program is free software; you can redistribute it and/or modify it
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
8 # option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful, but
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 # Public License for more details.
13 # Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License along
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
18
19 '''colorize output from some commands
19 '''colorize output from some commands
20
20
21 This extension modifies the status and resolve commands to add color
21 This extension modifies the status and resolve commands to add color
22 to their output to reflect file status, the qseries command to add
22 to their output to reflect file status, the qseries command to add
23 color to reflect patch status (applied, unapplied, missing), and to
23 color to reflect patch status (applied, unapplied, missing), and to
24 diff-related commands to highlight additions, removals, diff headers,
24 diff-related commands to highlight additions, removals, diff headers,
25 and trailing whitespace.
25 and trailing whitespace.
26
26
27 Other effects in addition to color, like bold and underlined text, are
27 Other effects in addition to color, like bold and underlined text, are
28 also available. By default, the terminfo database is used to find the
28 also available. By default, the terminfo database is used to find the
29 terminal codes used to change color and effect. If terminfo is not
29 terminal codes used to change color and effect. If terminfo is not
30 available, then effects are rendered with the ECMA-48 SGR control
30 available, then effects are rendered with the ECMA-48 SGR control
31 function (aka ANSI escape codes).
31 function (aka ANSI escape codes).
32
32
33 Default effects may be overridden from your configuration file::
33 Default effects may be overridden from your configuration file::
34
34
35 [color]
35 [color]
36 status.modified = blue bold underline red_background
36 status.modified = blue bold underline red_background
37 status.added = green bold
37 status.added = green bold
38 status.removed = red bold blue_background
38 status.removed = red bold blue_background
39 status.deleted = cyan bold underline
39 status.deleted = cyan bold underline
40 status.unknown = magenta bold underline
40 status.unknown = magenta bold underline
41 status.ignored = black bold
41 status.ignored = black bold
42
42
43 # 'none' turns off all effects
43 # 'none' turns off all effects
44 status.clean = none
44 status.clean = none
45 status.copied = none
45 status.copied = none
46
46
47 qseries.applied = blue bold underline
47 qseries.applied = blue bold underline
48 qseries.unapplied = black bold
48 qseries.unapplied = black bold
49 qseries.missing = red bold
49 qseries.missing = red bold
50
50
51 diff.diffline = bold
51 diff.diffline = bold
52 diff.extended = cyan bold
52 diff.extended = cyan bold
53 diff.file_a = red bold
53 diff.file_a = red bold
54 diff.file_b = green bold
54 diff.file_b = green bold
55 diff.hunk = magenta
55 diff.hunk = magenta
56 diff.deleted = red
56 diff.deleted = red
57 diff.inserted = green
57 diff.inserted = green
58 diff.changed = white
58 diff.changed = white
59 diff.trailingwhitespace = bold red_background
59 diff.trailingwhitespace = bold red_background
60
60
61 resolve.unresolved = red bold
61 resolve.unresolved = red bold
62 resolve.resolved = green bold
62 resolve.resolved = green bold
63
63
64 bookmarks.current = green
64 bookmarks.current = green
65
65
66 branches.active = none
66 branches.active = none
67 branches.closed = black bold
67 branches.closed = black bold
68 branches.current = green
68 branches.current = green
69 branches.inactive = none
69 branches.inactive = none
70
70
71 The available effects in terminfo mode are 'blink', 'bold', 'dim',
71 The available effects in terminfo mode are 'blink', 'bold', 'dim',
72 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
72 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
73 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
73 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
74 'underline'. How each is rendered depends on the terminal emulator.
74 'underline'. How each is rendered depends on the terminal emulator.
75 Some may not be available for a given terminal type, and will be
75 Some may not be available for a given terminal type, and will be
76 silently ignored.
76 silently ignored.
77
77
78 Because there are only eight standard colors, this module allows you
78 Because there are only eight standard colors, this module allows you
79 to define color names for other color slots which might be available
79 to define color names for other color slots which might be available
80 for your terminal type, assuming terminfo mode. For instance::
80 for your terminal type, assuming terminfo mode. For instance::
81
81
82 color.brightblue = 12
82 color.brightblue = 12
83 color.pink = 207
83 color.pink = 207
84 color.orange = 202
84 color.orange = 202
85
85
86 to set 'brightblue' to color slot 12 (useful for 16 color terminals
86 to set 'brightblue' to color slot 12 (useful for 16 color terminals
87 that have brighter colors defined in the upper eight) and, 'pink' and
87 that have brighter colors defined in the upper eight) and, 'pink' and
88 'orange' to colors in 256-color xterm's default color cube. These
88 'orange' to colors in 256-color xterm's default color cube. These
89 defined colors may then be used as any of the pre-defined eight,
89 defined colors may then be used as any of the pre-defined eight,
90 including appending '_background' to set the background to that color.
90 including appending '_background' to set the background to that color.
91
91
92 The color extension will try to detect whether to use terminfo, ANSI
92 The color extension will try to detect whether to use terminfo, ANSI
93 codes or Win32 console APIs, unless it is made explicit; e.g.::
93 codes or Win32 console APIs, unless it is made explicit; e.g.::
94
94
95 [color]
95 [color]
96 mode = ansi
96 mode = ansi
97
97
98 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
98 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
99 disable color.
99 disable color.
100
100
101 '''
101 '''
102
102
103 import os, sys
103 import os
104
104
105 from mercurial import commands, dispatch, extensions, ui as uimod, util
105 from mercurial import commands, dispatch, extensions, ui as uimod, util
106 from mercurial.i18n import _
106 from mercurial.i18n import _
107
107
108 # start and stop parameters for effects
108 # start and stop parameters for effects
109 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
109 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
110 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
110 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
111 'italic': 3, 'underline': 4, 'inverse': 7,
111 'italic': 3, 'underline': 4, 'inverse': 7,
112 'black_background': 40, 'red_background': 41,
112 'black_background': 40, 'red_background': 41,
113 'green_background': 42, 'yellow_background': 43,
113 'green_background': 42, 'yellow_background': 43,
114 'blue_background': 44, 'purple_background': 45,
114 'blue_background': 44, 'purple_background': 45,
115 'cyan_background': 46, 'white_background': 47}
115 'cyan_background': 46, 'white_background': 47}
116
116
117 def _terminfosetup(ui):
117 def _terminfosetup(ui):
118 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
118 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
119
119
120 global _terminfo_params
120 global _terminfo_params
121 # If we failed to load curses, we go ahead and return.
121 # If we failed to load curses, we go ahead and return.
122 if not _terminfo_params:
122 if not _terminfo_params:
123 return
123 return
124 # Otherwise, see what the config file says.
124 # Otherwise, see what the config file says.
125 mode = ui.config('color', 'mode', 'auto')
125 mode = ui.config('color', 'mode', 'auto')
126 if mode not in ('auto', 'terminfo'):
126 if mode not in ('auto', 'terminfo'):
127 return
127 return
128
128
129 _terminfo_params.update((key[6:], (False, int(val)))
129 _terminfo_params.update((key[6:], (False, int(val)))
130 for key, val in ui.configitems('color')
130 for key, val in ui.configitems('color')
131 if key.startswith('color.'))
131 if key.startswith('color.'))
132
132
133 try:
133 try:
134 curses.setupterm()
134 curses.setupterm()
135 except curses.error, e:
135 except curses.error, e:
136 _terminfo_params = {}
136 _terminfo_params = {}
137 return
137 return
138
138
139 for key, (b, e) in _terminfo_params.items():
139 for key, (b, e) in _terminfo_params.items():
140 if not b:
140 if not b:
141 continue
141 continue
142 if not curses.tigetstr(e):
142 if not curses.tigetstr(e):
143 # Most terminals don't support dim, invis, etc, so don't be
143 # Most terminals don't support dim, invis, etc, so don't be
144 # noisy and use ui.debug().
144 # noisy and use ui.debug().
145 ui.debug("no terminfo entry for %s\n" % e)
145 ui.debug("no terminfo entry for %s\n" % e)
146 del _terminfo_params[key]
146 del _terminfo_params[key]
147 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
147 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
148 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
148 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
149 "ECMA-48 color\n"))
149 "ECMA-48 color\n"))
150 _terminfo_params = {}
150 _terminfo_params = {}
151
151
152 try:
152 try:
153 import curses
153 import curses
154 # Mapping from effect name to terminfo attribute name or color number.
154 # Mapping from effect name to terminfo attribute name or color number.
155 # This will also force-load the curses module.
155 # This will also force-load the curses module.
156 _terminfo_params = {'none': (True, 'sgr0'),
156 _terminfo_params = {'none': (True, 'sgr0'),
157 'standout': (True, 'smso'),
157 'standout': (True, 'smso'),
158 'underline': (True, 'smul'),
158 'underline': (True, 'smul'),
159 'reverse': (True, 'rev'),
159 'reverse': (True, 'rev'),
160 'inverse': (True, 'rev'),
160 'inverse': (True, 'rev'),
161 'blink': (True, 'blink'),
161 'blink': (True, 'blink'),
162 'dim': (True, 'dim'),
162 'dim': (True, 'dim'),
163 'bold': (True, 'bold'),
163 'bold': (True, 'bold'),
164 'invisible': (True, 'invis'),
164 'invisible': (True, 'invis'),
165 'italic': (True, 'sitm'),
165 'italic': (True, 'sitm'),
166 'black': (False, curses.COLOR_BLACK),
166 'black': (False, curses.COLOR_BLACK),
167 'red': (False, curses.COLOR_RED),
167 'red': (False, curses.COLOR_RED),
168 'green': (False, curses.COLOR_GREEN),
168 'green': (False, curses.COLOR_GREEN),
169 'yellow': (False, curses.COLOR_YELLOW),
169 'yellow': (False, curses.COLOR_YELLOW),
170 'blue': (False, curses.COLOR_BLUE),
170 'blue': (False, curses.COLOR_BLUE),
171 'magenta': (False, curses.COLOR_MAGENTA),
171 'magenta': (False, curses.COLOR_MAGENTA),
172 'cyan': (False, curses.COLOR_CYAN),
172 'cyan': (False, curses.COLOR_CYAN),
173 'white': (False, curses.COLOR_WHITE)}
173 'white': (False, curses.COLOR_WHITE)}
174 except ImportError:
174 except ImportError:
175 _terminfo_params = False
175 _terminfo_params = False
176
176
177 _styles = {'grep.match': 'red bold',
177 _styles = {'grep.match': 'red bold',
178 'bookmarks.current': 'green',
178 'bookmarks.current': 'green',
179 'branches.active': 'none',
179 'branches.active': 'none',
180 'branches.closed': 'black bold',
180 'branches.closed': 'black bold',
181 'branches.current': 'green',
181 'branches.current': 'green',
182 'branches.inactive': 'none',
182 'branches.inactive': 'none',
183 'diff.changed': 'white',
183 'diff.changed': 'white',
184 'diff.deleted': 'red',
184 'diff.deleted': 'red',
185 'diff.diffline': 'bold',
185 'diff.diffline': 'bold',
186 'diff.extended': 'cyan bold',
186 'diff.extended': 'cyan bold',
187 'diff.file_a': 'red bold',
187 'diff.file_a': 'red bold',
188 'diff.file_b': 'green bold',
188 'diff.file_b': 'green bold',
189 'diff.hunk': 'magenta',
189 'diff.hunk': 'magenta',
190 'diff.inserted': 'green',
190 'diff.inserted': 'green',
191 'diff.trailingwhitespace': 'bold red_background',
191 'diff.trailingwhitespace': 'bold red_background',
192 'diffstat.deleted': 'red',
192 'diffstat.deleted': 'red',
193 'diffstat.inserted': 'green',
193 'diffstat.inserted': 'green',
194 'ui.prompt': 'yellow',
194 'ui.prompt': 'yellow',
195 'log.changeset': 'yellow',
195 'log.changeset': 'yellow',
196 'resolve.resolved': 'green bold',
196 'resolve.resolved': 'green bold',
197 'resolve.unresolved': 'red bold',
197 'resolve.unresolved': 'red bold',
198 'status.added': 'green bold',
198 'status.added': 'green bold',
199 'status.clean': 'none',
199 'status.clean': 'none',
200 'status.copied': 'none',
200 'status.copied': 'none',
201 'status.deleted': 'cyan bold underline',
201 'status.deleted': 'cyan bold underline',
202 'status.ignored': 'black bold',
202 'status.ignored': 'black bold',
203 'status.modified': 'blue bold',
203 'status.modified': 'blue bold',
204 'status.removed': 'red bold',
204 'status.removed': 'red bold',
205 'status.unknown': 'magenta bold underline'}
205 'status.unknown': 'magenta bold underline'}
206
206
207
207
208 def _effect_str(effect):
208 def _effect_str(effect):
209 '''Helper function for render_effects().'''
209 '''Helper function for render_effects().'''
210
210
211 bg = False
211 bg = False
212 if effect.endswith('_background'):
212 if effect.endswith('_background'):
213 bg = True
213 bg = True
214 effect = effect[:-11]
214 effect = effect[:-11]
215 attr, val = _terminfo_params[effect]
215 attr, val = _terminfo_params[effect]
216 if attr:
216 if attr:
217 return curses.tigetstr(val)
217 return curses.tigetstr(val)
218 elif bg:
218 elif bg:
219 return curses.tparm(curses.tigetstr('setab'), val)
219 return curses.tparm(curses.tigetstr('setab'), val)
220 else:
220 else:
221 return curses.tparm(curses.tigetstr('setaf'), val)
221 return curses.tparm(curses.tigetstr('setaf'), val)
222
222
223 def render_effects(text, effects):
223 def render_effects(text, effects):
224 'Wrap text in commands to turn on each effect.'
224 'Wrap text in commands to turn on each effect.'
225 if not text:
225 if not text:
226 return text
226 return text
227 if not _terminfo_params:
227 if not _terminfo_params:
228 start = [str(_effects[e]) for e in ['none'] + effects.split()]
228 start = [str(_effects[e]) for e in ['none'] + effects.split()]
229 start = '\033[' + ';'.join(start) + 'm'
229 start = '\033[' + ';'.join(start) + 'm'
230 stop = '\033[' + str(_effects['none']) + 'm'
230 stop = '\033[' + str(_effects['none']) + 'm'
231 else:
231 else:
232 start = ''.join(_effect_str(effect)
232 start = ''.join(_effect_str(effect)
233 for effect in ['none'] + effects.split())
233 for effect in ['none'] + effects.split())
234 stop = _effect_str('none')
234 stop = _effect_str('none')
235 return ''.join([start, text, stop])
235 return ''.join([start, text, stop])
236
236
237 def extstyles():
237 def extstyles():
238 for name, ext in extensions.extensions():
238 for name, ext in extensions.extensions():
239 _styles.update(getattr(ext, 'colortable', {}))
239 _styles.update(getattr(ext, 'colortable', {}))
240
240
241 def configstyles(ui):
241 def configstyles(ui):
242 for status, cfgeffects in ui.configitems('color'):
242 for status, cfgeffects in ui.configitems('color'):
243 if '.' not in status or status.startswith('color.'):
243 if '.' not in status or status.startswith('color.'):
244 continue
244 continue
245 cfgeffects = ui.configlist('color', status)
245 cfgeffects = ui.configlist('color', status)
246 if cfgeffects:
246 if cfgeffects:
247 good = []
247 good = []
248 for e in cfgeffects:
248 for e in cfgeffects:
249 if not _terminfo_params and e in _effects:
249 if not _terminfo_params and e in _effects:
250 good.append(e)
250 good.append(e)
251 elif e in _terminfo_params or e[:-11] in _terminfo_params:
251 elif e in _terminfo_params or e[:-11] in _terminfo_params:
252 good.append(e)
252 good.append(e)
253 else:
253 else:
254 ui.warn(_("ignoring unknown color/effect %r "
254 ui.warn(_("ignoring unknown color/effect %r "
255 "(configured in color.%s)\n")
255 "(configured in color.%s)\n")
256 % (e, status))
256 % (e, status))
257 _styles[status] = ' '.join(good)
257 _styles[status] = ' '.join(good)
258
258
259 class colorui(uimod.ui):
259 class colorui(uimod.ui):
260 def popbuffer(self, labeled=False):
260 def popbuffer(self, labeled=False):
261 if labeled:
261 if labeled:
262 return ''.join(self.label(a, label) for a, label
262 return ''.join(self.label(a, label) for a, label
263 in self._buffers.pop())
263 in self._buffers.pop())
264 return ''.join(a for a, label in self._buffers.pop())
264 return ''.join(a for a, label in self._buffers.pop())
265
265
266 _colormode = 'ansi'
266 _colormode = 'ansi'
267 def write(self, *args, **opts):
267 def write(self, *args, **opts):
268 label = opts.get('label', '')
268 label = opts.get('label', '')
269 if self._buffers:
269 if self._buffers:
270 self._buffers[-1].extend([(str(a), label) for a in args])
270 self._buffers[-1].extend([(str(a), label) for a in args])
271 elif self._colormode == 'win32':
271 elif self._colormode == 'win32':
272 for a in args:
272 for a in args:
273 win32print(a, super(colorui, self).write, **opts)
273 win32print(a, super(colorui, self).write, **opts)
274 else:
274 else:
275 return super(colorui, self).write(
275 return super(colorui, self).write(
276 *[self.label(str(a), label) for a in args], **opts)
276 *[self.label(str(a), label) for a in args], **opts)
277
277
278 def write_err(self, *args, **opts):
278 def write_err(self, *args, **opts):
279 label = opts.get('label', '')
279 label = opts.get('label', '')
280 if self._colormode == 'win32':
280 if self._colormode == 'win32':
281 for a in args:
281 for a in args:
282 win32print(a, super(colorui, self).write_err, **opts)
282 win32print(a, super(colorui, self).write_err, **opts)
283 else:
283 else:
284 return super(colorui, self).write_err(
284 return super(colorui, self).write_err(
285 *[self.label(str(a), label) for a in args], **opts)
285 *[self.label(str(a), label) for a in args], **opts)
286
286
287 def label(self, msg, label):
287 def label(self, msg, label):
288 effects = []
288 effects = []
289 for l in label.split():
289 for l in label.split():
290 s = _styles.get(l, '')
290 s = _styles.get(l, '')
291 if s:
291 if s:
292 effects.append(s)
292 effects.append(s)
293 effects = ''.join(effects)
293 effects = ''.join(effects)
294 if effects:
294 if effects:
295 return '\n'.join([render_effects(s, effects)
295 return '\n'.join([render_effects(s, effects)
296 for s in msg.split('\n')])
296 for s in msg.split('\n')])
297 return msg
297 return msg
298
298
299
299
300 def uisetup(ui):
300 def uisetup(ui):
301 global _terminfo_params
301 global _terminfo_params
302 if ui.plain():
302 if ui.plain():
303 return
303 return
304
304
305 formatted = (os.environ.get('TERM') != 'dumb' and ui.formatted())
305 formatted = (os.environ.get('TERM') != 'dumb' and ui.formatted())
306 mode = ui.config('color', 'mode', 'auto')
306 mode = ui.config('color', 'mode', 'auto')
307 if mode == 'auto':
307 if mode == 'auto':
308 if os.name == 'nt' and 'TERM' not in os.environ:
308 if os.name == 'nt' and 'TERM' not in os.environ:
309 # looks line a cmd.exe console, use win32 API or nothing
309 # looks line a cmd.exe console, use win32 API or nothing
310 mode = w32effects and 'win32' or 'none'
310 mode = w32effects and 'win32' or 'none'
311 else:
311 else:
312 if not formatted:
312 if not formatted:
313 _terminfo_params = False
313 _terminfo_params = False
314 else:
314 else:
315 _terminfosetup(ui)
315 _terminfosetup(ui)
316 if not _terminfo_params:
316 if not _terminfo_params:
317 mode = 'ansi'
317 mode = 'ansi'
318 else:
318 else:
319 mode = 'terminfo'
319 mode = 'terminfo'
320 if mode == 'win32':
320 if mode == 'win32':
321 if w32effects is None:
321 if w32effects is None:
322 # only warn if color.mode is explicitly set to win32
322 # only warn if color.mode is explicitly set to win32
323 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
323 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
324 return
324 return
325 _effects.update(w32effects)
325 _effects.update(w32effects)
326 elif mode == 'ansi':
326 elif mode == 'ansi':
327 _terminfo_params = {}
327 _terminfo_params = {}
328 elif mode == 'terminfo':
328 elif mode == 'terminfo':
329 _terminfosetup(ui)
329 _terminfosetup(ui)
330 else:
330 else:
331 return
331 return
332 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
332 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
333 coloropt = opts['color']
333 coloropt = opts['color']
334 auto = coloropt == 'auto'
334 auto = coloropt == 'auto'
335 always = util.parsebool(coloropt)
335 always = util.parsebool(coloropt)
336 if (always or
336 if (always or
337 (always is None and auto and formatted)):
337 (always is None and auto and formatted)):
338 colorui._colormode = mode
338 colorui._colormode = mode
339 colorui.__bases__ = (ui_.__class__,)
339 colorui.__bases__ = (ui_.__class__,)
340 ui_.__class__ = colorui
340 ui_.__class__ = colorui
341 extstyles()
341 extstyles()
342 configstyles(ui_)
342 configstyles(ui_)
343 return orig(ui_, opts, cmd, cmdfunc)
343 return orig(ui_, opts, cmd, cmdfunc)
344 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
344 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
345
345
346 def extsetup(ui):
346 def extsetup(ui):
347 commands.globalopts.append(
347 commands.globalopts.append(
348 ('', 'color', 'auto',
348 ('', 'color', 'auto',
349 # i18n: 'always', 'auto', and 'never' are keywords and should
349 # i18n: 'always', 'auto', and 'never' are keywords and should
350 # not be translated
350 # not be translated
351 _("when to colorize (boolean, always, auto, or never)"),
351 _("when to colorize (boolean, always, auto, or never)"),
352 _('TYPE')))
352 _('TYPE')))
353
353
354 if os.name != 'nt':
354 if os.name != 'nt':
355 w32effects = None
355 w32effects = None
356 else:
356 else:
357 import re, ctypes
357 import re, ctypes
358
358
359 _kernel32 = ctypes.windll.kernel32
359 _kernel32 = ctypes.windll.kernel32
360
360
361 _WORD = ctypes.c_ushort
361 _WORD = ctypes.c_ushort
362
362
363 _INVALID_HANDLE_VALUE = -1
363 _INVALID_HANDLE_VALUE = -1
364
364
365 class _COORD(ctypes.Structure):
365 class _COORD(ctypes.Structure):
366 _fields_ = [('X', ctypes.c_short),
366 _fields_ = [('X', ctypes.c_short),
367 ('Y', ctypes.c_short)]
367 ('Y', ctypes.c_short)]
368
368
369 class _SMALL_RECT(ctypes.Structure):
369 class _SMALL_RECT(ctypes.Structure):
370 _fields_ = [('Left', ctypes.c_short),
370 _fields_ = [('Left', ctypes.c_short),
371 ('Top', ctypes.c_short),
371 ('Top', ctypes.c_short),
372 ('Right', ctypes.c_short),
372 ('Right', ctypes.c_short),
373 ('Bottom', ctypes.c_short)]
373 ('Bottom', ctypes.c_short)]
374
374
375 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
375 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
376 _fields_ = [('dwSize', _COORD),
376 _fields_ = [('dwSize', _COORD),
377 ('dwCursorPosition', _COORD),
377 ('dwCursorPosition', _COORD),
378 ('wAttributes', _WORD),
378 ('wAttributes', _WORD),
379 ('srWindow', _SMALL_RECT),
379 ('srWindow', _SMALL_RECT),
380 ('dwMaximumWindowSize', _COORD)]
380 ('dwMaximumWindowSize', _COORD)]
381
381
382 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
382 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
383 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
383 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
384
384
385 _FOREGROUND_BLUE = 0x0001
385 _FOREGROUND_BLUE = 0x0001
386 _FOREGROUND_GREEN = 0x0002
386 _FOREGROUND_GREEN = 0x0002
387 _FOREGROUND_RED = 0x0004
387 _FOREGROUND_RED = 0x0004
388 _FOREGROUND_INTENSITY = 0x0008
388 _FOREGROUND_INTENSITY = 0x0008
389
389
390 _BACKGROUND_BLUE = 0x0010
390 _BACKGROUND_BLUE = 0x0010
391 _BACKGROUND_GREEN = 0x0020
391 _BACKGROUND_GREEN = 0x0020
392 _BACKGROUND_RED = 0x0040
392 _BACKGROUND_RED = 0x0040
393 _BACKGROUND_INTENSITY = 0x0080
393 _BACKGROUND_INTENSITY = 0x0080
394
394
395 _COMMON_LVB_REVERSE_VIDEO = 0x4000
395 _COMMON_LVB_REVERSE_VIDEO = 0x4000
396 _COMMON_LVB_UNDERSCORE = 0x8000
396 _COMMON_LVB_UNDERSCORE = 0x8000
397
397
398 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
398 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
399 w32effects = {
399 w32effects = {
400 'none': -1,
400 'none': -1,
401 'black': 0,
401 'black': 0,
402 'red': _FOREGROUND_RED,
402 'red': _FOREGROUND_RED,
403 'green': _FOREGROUND_GREEN,
403 'green': _FOREGROUND_GREEN,
404 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
404 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
405 'blue': _FOREGROUND_BLUE,
405 'blue': _FOREGROUND_BLUE,
406 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
406 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
407 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
407 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
408 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
408 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
409 'bold': _FOREGROUND_INTENSITY,
409 'bold': _FOREGROUND_INTENSITY,
410 'black_background': 0x100, # unused value > 0x0f
410 'black_background': 0x100, # unused value > 0x0f
411 'red_background': _BACKGROUND_RED,
411 'red_background': _BACKGROUND_RED,
412 'green_background': _BACKGROUND_GREEN,
412 'green_background': _BACKGROUND_GREEN,
413 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
413 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
414 'blue_background': _BACKGROUND_BLUE,
414 'blue_background': _BACKGROUND_BLUE,
415 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
415 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
416 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
416 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
417 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
417 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
418 _BACKGROUND_BLUE),
418 _BACKGROUND_BLUE),
419 'bold_background': _BACKGROUND_INTENSITY,
419 'bold_background': _BACKGROUND_INTENSITY,
420 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
420 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
421 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
421 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
422 }
422 }
423
423
424 passthrough = set([_FOREGROUND_INTENSITY,
424 passthrough = set([_FOREGROUND_INTENSITY,
425 _BACKGROUND_INTENSITY,
425 _BACKGROUND_INTENSITY,
426 _COMMON_LVB_UNDERSCORE,
426 _COMMON_LVB_UNDERSCORE,
427 _COMMON_LVB_REVERSE_VIDEO])
427 _COMMON_LVB_REVERSE_VIDEO])
428
428
429 stdout = _kernel32.GetStdHandle(
429 stdout = _kernel32.GetStdHandle(
430 _STD_OUTPUT_HANDLE) # don't close the handle returned
430 _STD_OUTPUT_HANDLE) # don't close the handle returned
431 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
431 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
432 w32effects = None
432 w32effects = None
433 else:
433 else:
434 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
434 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
435 if not _kernel32.GetConsoleScreenBufferInfo(
435 if not _kernel32.GetConsoleScreenBufferInfo(
436 stdout, ctypes.byref(csbi)):
436 stdout, ctypes.byref(csbi)):
437 # stdout may not support GetConsoleScreenBufferInfo()
437 # stdout may not support GetConsoleScreenBufferInfo()
438 # when called from subprocess or redirected
438 # when called from subprocess or redirected
439 w32effects = None
439 w32effects = None
440 else:
440 else:
441 origattr = csbi.wAttributes
441 origattr = csbi.wAttributes
442 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
442 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
443 re.MULTILINE | re.DOTALL)
443 re.MULTILINE | re.DOTALL)
444
444
445 def win32print(text, orig, **opts):
445 def win32print(text, orig, **opts):
446 label = opts.get('label', '')
446 label = opts.get('label', '')
447 attr = origattr
447 attr = origattr
448
448
449 def mapcolor(val, attr):
449 def mapcolor(val, attr):
450 if val == -1:
450 if val == -1:
451 return origattr
451 return origattr
452 elif val in passthrough:
452 elif val in passthrough:
453 return attr | val
453 return attr | val
454 elif val > 0x0f:
454 elif val > 0x0f:
455 return (val & 0x70) | (attr & 0x8f)
455 return (val & 0x70) | (attr & 0x8f)
456 else:
456 else:
457 return (val & 0x07) | (attr & 0xf8)
457 return (val & 0x07) | (attr & 0xf8)
458
458
459 # determine console attributes based on labels
459 # determine console attributes based on labels
460 for l in label.split():
460 for l in label.split():
461 style = _styles.get(l, '')
461 style = _styles.get(l, '')
462 for effect in style.split():
462 for effect in style.split():
463 attr = mapcolor(w32effects[effect], attr)
463 attr = mapcolor(w32effects[effect], attr)
464
464
465 # hack to ensure regexp finds data
465 # hack to ensure regexp finds data
466 if not text.startswith('\033['):
466 if not text.startswith('\033['):
467 text = '\033[m' + text
467 text = '\033[m' + text
468
468
469 # Look for ANSI-like codes embedded in text
469 # Look for ANSI-like codes embedded in text
470 m = re.match(ansire, text)
470 m = re.match(ansire, text)
471
471
472 try:
472 try:
473 while m:
473 while m:
474 for sattr in m.group(1).split(';'):
474 for sattr in m.group(1).split(';'):
475 if sattr:
475 if sattr:
476 attr = mapcolor(int(sattr), attr)
476 attr = mapcolor(int(sattr), attr)
477 _kernel32.SetConsoleTextAttribute(stdout, attr)
477 _kernel32.SetConsoleTextAttribute(stdout, attr)
478 orig(m.group(2), **opts)
478 orig(m.group(2), **opts)
479 m = re.match(ansire, m.group(3))
479 m = re.match(ansire, m.group(3))
480 finally:
480 finally:
481 # Explicity reset original attributes
481 # Explicity reset original attributes
482 _kernel32.SetConsoleTextAttribute(stdout, origattr)
482 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,200 +1,200
1 # darcs.py - darcs support for the convert extension
1 # darcs.py - darcs support for the convert extension
2 #
2 #
3 # Copyright 2007-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2007-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from common import NoRepo, checktool, commandline, commit, converter_source
8 from common import NoRepo, checktool, commandline, commit, converter_source
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10 from mercurial import encoding, util
10 from mercurial import util
11 import os, shutil, tempfile, re
11 import os, shutil, tempfile, re
12
12
13 # The naming drift of ElementTree is fun!
13 # The naming drift of ElementTree is fun!
14
14
15 try:
15 try:
16 from xml.etree.cElementTree import ElementTree, XMLParser
16 from xml.etree.cElementTree import ElementTree, XMLParser
17 except ImportError:
17 except ImportError:
18 try:
18 try:
19 from xml.etree.ElementTree import ElementTree, XMLParser
19 from xml.etree.ElementTree import ElementTree, XMLParser
20 except ImportError:
20 except ImportError:
21 try:
21 try:
22 from elementtree.cElementTree import ElementTree, XMLParser
22 from elementtree.cElementTree import ElementTree, XMLParser
23 except ImportError:
23 except ImportError:
24 try:
24 try:
25 from elementtree.ElementTree import ElementTree, XMLParser
25 from elementtree.ElementTree import ElementTree, XMLParser
26 except ImportError:
26 except ImportError:
27 ElementTree = None
27 ElementTree = None
28
28
29 class darcs_source(converter_source, commandline):
29 class darcs_source(converter_source, commandline):
30 def __init__(self, ui, path, rev=None):
30 def __init__(self, ui, path, rev=None):
31 converter_source.__init__(self, ui, path, rev=rev)
31 converter_source.__init__(self, ui, path, rev=rev)
32 commandline.__init__(self, ui, 'darcs')
32 commandline.__init__(self, ui, 'darcs')
33
33
34 # check for _darcs, ElementTree so that we can easily skip
34 # check for _darcs, ElementTree so that we can easily skip
35 # test-convert-darcs if ElementTree is not around
35 # test-convert-darcs if ElementTree is not around
36 if not os.path.exists(os.path.join(path, '_darcs')):
36 if not os.path.exists(os.path.join(path, '_darcs')):
37 raise NoRepo(_("%s does not look like a darcs repository") % path)
37 raise NoRepo(_("%s does not look like a darcs repository") % path)
38
38
39 checktool('darcs')
39 checktool('darcs')
40 version = self.run0('--version').splitlines()[0].strip()
40 version = self.run0('--version').splitlines()[0].strip()
41 if version < '2.1':
41 if version < '2.1':
42 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
42 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
43 version)
43 version)
44
44
45 if ElementTree is None:
45 if ElementTree is None:
46 raise util.Abort(_("Python ElementTree module is not available"))
46 raise util.Abort(_("Python ElementTree module is not available"))
47
47
48 self.path = os.path.realpath(path)
48 self.path = os.path.realpath(path)
49
49
50 self.lastrev = None
50 self.lastrev = None
51 self.changes = {}
51 self.changes = {}
52 self.parents = {}
52 self.parents = {}
53 self.tags = {}
53 self.tags = {}
54
54
55 # Check darcs repository format
55 # Check darcs repository format
56 format = self.format()
56 format = self.format()
57 if format:
57 if format:
58 if format in ('darcs-1.0', 'hashed'):
58 if format in ('darcs-1.0', 'hashed'):
59 raise NoRepo(_("%s repository format is unsupported, "
59 raise NoRepo(_("%s repository format is unsupported, "
60 "please upgrade") % format)
60 "please upgrade") % format)
61 else:
61 else:
62 self.ui.warn(_('failed to detect repository format!'))
62 self.ui.warn(_('failed to detect repository format!'))
63
63
64 def before(self):
64 def before(self):
65 self.tmppath = tempfile.mkdtemp(
65 self.tmppath = tempfile.mkdtemp(
66 prefix='convert-' + os.path.basename(self.path) + '-')
66 prefix='convert-' + os.path.basename(self.path) + '-')
67 output, status = self.run('init', repodir=self.tmppath)
67 output, status = self.run('init', repodir=self.tmppath)
68 self.checkexit(status)
68 self.checkexit(status)
69
69
70 tree = self.xml('changes', xml_output=True, summary=True,
70 tree = self.xml('changes', xml_output=True, summary=True,
71 repodir=self.path)
71 repodir=self.path)
72 tagname = None
72 tagname = None
73 child = None
73 child = None
74 for elt in tree.findall('patch'):
74 for elt in tree.findall('patch'):
75 node = elt.get('hash')
75 node = elt.get('hash')
76 name = elt.findtext('name', '')
76 name = elt.findtext('name', '')
77 if name.startswith('TAG '):
77 if name.startswith('TAG '):
78 tagname = name[4:].strip()
78 tagname = name[4:].strip()
79 elif tagname is not None:
79 elif tagname is not None:
80 self.tags[tagname] = node
80 self.tags[tagname] = node
81 tagname = None
81 tagname = None
82 self.changes[node] = elt
82 self.changes[node] = elt
83 self.parents[child] = [node]
83 self.parents[child] = [node]
84 child = node
84 child = node
85 self.parents[child] = []
85 self.parents[child] = []
86
86
87 def after(self):
87 def after(self):
88 self.ui.debug('cleaning up %s\n' % self.tmppath)
88 self.ui.debug('cleaning up %s\n' % self.tmppath)
89 shutil.rmtree(self.tmppath, ignore_errors=True)
89 shutil.rmtree(self.tmppath, ignore_errors=True)
90
90
91 def recode(self, s, encoding=None):
91 def recode(self, s, encoding=None):
92 if isinstance(s, unicode):
92 if isinstance(s, unicode):
93 # XMLParser returns unicode objects for anything it can't
93 # XMLParser returns unicode objects for anything it can't
94 # encode into ASCII. We convert them back to str to get
94 # encode into ASCII. We convert them back to str to get
95 # recode's normal conversion behavior.
95 # recode's normal conversion behavior.
96 s = s.encode('latin-1')
96 s = s.encode('latin-1')
97 return super(darcs_source, self).recode(s, encoding)
97 return super(darcs_source, self).recode(s, encoding)
98
98
99 def xml(self, cmd, **kwargs):
99 def xml(self, cmd, **kwargs):
100 # NOTE: darcs is currently encoding agnostic and will print
100 # NOTE: darcs is currently encoding agnostic and will print
101 # patch metadata byte-for-byte, even in the XML changelog.
101 # patch metadata byte-for-byte, even in the XML changelog.
102 etree = ElementTree()
102 etree = ElementTree()
103 # While we are decoding the XML as latin-1 to be as liberal as
103 # While we are decoding the XML as latin-1 to be as liberal as
104 # possible, etree will still raise an exception if any
104 # possible, etree will still raise an exception if any
105 # non-printable characters are in the XML changelog.
105 # non-printable characters are in the XML changelog.
106 parser = XMLParser(encoding='latin-1')
106 parser = XMLParser(encoding='latin-1')
107 fp = self._run(cmd, **kwargs)
107 fp = self._run(cmd, **kwargs)
108 etree.parse(fp, parser=parser)
108 etree.parse(fp, parser=parser)
109 self.checkexit(fp.close())
109 self.checkexit(fp.close())
110 return etree.getroot()
110 return etree.getroot()
111
111
112 def format(self):
112 def format(self):
113 output, status = self.run('show', 'repo', no_files=True,
113 output, status = self.run('show', 'repo', no_files=True,
114 repodir=self.path)
114 repodir=self.path)
115 self.checkexit(status)
115 self.checkexit(status)
116 m = re.search(r'^\s*Format:\s*(.*)$', output, re.MULTILINE)
116 m = re.search(r'^\s*Format:\s*(.*)$', output, re.MULTILINE)
117 if not m:
117 if not m:
118 return None
118 return None
119 return ','.join(sorted(f.strip() for f in m.group(1).split(',')))
119 return ','.join(sorted(f.strip() for f in m.group(1).split(',')))
120
120
121 def manifest(self):
121 def manifest(self):
122 man = []
122 man = []
123 output, status = self.run('show', 'files', no_directories=True,
123 output, status = self.run('show', 'files', no_directories=True,
124 repodir=self.tmppath)
124 repodir=self.tmppath)
125 self.checkexit(status)
125 self.checkexit(status)
126 for line in output.split('\n'):
126 for line in output.split('\n'):
127 path = line[2:]
127 path = line[2:]
128 if path:
128 if path:
129 man.append(path)
129 man.append(path)
130 return man
130 return man
131
131
132 def getheads(self):
132 def getheads(self):
133 return self.parents[None]
133 return self.parents[None]
134
134
135 def getcommit(self, rev):
135 def getcommit(self, rev):
136 elt = self.changes[rev]
136 elt = self.changes[rev]
137 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
137 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
138 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
138 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
139 # etree can return unicode objects for name, comment, and author,
139 # etree can return unicode objects for name, comment, and author,
140 # so recode() is used to ensure str objects are emitted.
140 # so recode() is used to ensure str objects are emitted.
141 return commit(author=self.recode(elt.get('author')),
141 return commit(author=self.recode(elt.get('author')),
142 date=util.datestr(date),
142 date=util.datestr(date),
143 desc=self.recode(desc).strip(),
143 desc=self.recode(desc).strip(),
144 parents=self.parents[rev])
144 parents=self.parents[rev])
145
145
146 def pull(self, rev):
146 def pull(self, rev):
147 output, status = self.run('pull', self.path, all=True,
147 output, status = self.run('pull', self.path, all=True,
148 match='hash %s' % rev,
148 match='hash %s' % rev,
149 no_test=True, no_posthook=True,
149 no_test=True, no_posthook=True,
150 external_merge='/bin/false',
150 external_merge='/bin/false',
151 repodir=self.tmppath)
151 repodir=self.tmppath)
152 if status:
152 if status:
153 if output.find('We have conflicts in') == -1:
153 if output.find('We have conflicts in') == -1:
154 self.checkexit(status, output)
154 self.checkexit(status, output)
155 output, status = self.run('revert', all=True, repodir=self.tmppath)
155 output, status = self.run('revert', all=True, repodir=self.tmppath)
156 self.checkexit(status, output)
156 self.checkexit(status, output)
157
157
158 def getchanges(self, rev):
158 def getchanges(self, rev):
159 copies = {}
159 copies = {}
160 changes = []
160 changes = []
161 man = None
161 man = None
162 for elt in self.changes[rev].find('summary').getchildren():
162 for elt in self.changes[rev].find('summary').getchildren():
163 if elt.tag in ('add_directory', 'remove_directory'):
163 if elt.tag in ('add_directory', 'remove_directory'):
164 continue
164 continue
165 if elt.tag == 'move':
165 if elt.tag == 'move':
166 if man is None:
166 if man is None:
167 man = self.manifest()
167 man = self.manifest()
168 source, dest = elt.get('from'), elt.get('to')
168 source, dest = elt.get('from'), elt.get('to')
169 if source in man:
169 if source in man:
170 # File move
170 # File move
171 changes.append((source, rev))
171 changes.append((source, rev))
172 changes.append((dest, rev))
172 changes.append((dest, rev))
173 copies[dest] = source
173 copies[dest] = source
174 else:
174 else:
175 # Directory move, deduce file moves from manifest
175 # Directory move, deduce file moves from manifest
176 source = source + '/'
176 source = source + '/'
177 for f in man:
177 for f in man:
178 if not f.startswith(source):
178 if not f.startswith(source):
179 continue
179 continue
180 fdest = dest + '/' + f[len(source):]
180 fdest = dest + '/' + f[len(source):]
181 changes.append((f, rev))
181 changes.append((f, rev))
182 changes.append((fdest, rev))
182 changes.append((fdest, rev))
183 copies[fdest] = f
183 copies[fdest] = f
184 else:
184 else:
185 changes.append((elt.text.strip(), rev))
185 changes.append((elt.text.strip(), rev))
186 self.pull(rev)
186 self.pull(rev)
187 self.lastrev = rev
187 self.lastrev = rev
188 return sorted(changes), copies
188 return sorted(changes), copies
189
189
190 def getfile(self, name, rev):
190 def getfile(self, name, rev):
191 if rev != self.lastrev:
191 if rev != self.lastrev:
192 raise util.Abort(_('internal calling inconsistency'))
192 raise util.Abort(_('internal calling inconsistency'))
193 path = os.path.join(self.tmppath, name)
193 path = os.path.join(self.tmppath, name)
194 data = open(path, 'rb').read()
194 data = open(path, 'rb').read()
195 mode = os.lstat(path).st_mode
195 mode = os.lstat(path).st_mode
196 mode = (mode & 0111) and 'x' or ''
196 mode = (mode & 0111) and 'x' or ''
197 return data, mode
197 return data, mode
198
198
199 def gettags(self):
199 def gettags(self):
200 return self.tags
200 return self.tags
@@ -1,399 +1,399
1 # ASCII graph log extension for Mercurial
1 # ASCII graph log extension for Mercurial
2 #
2 #
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to view revision graphs from a shell
8 '''command to view revision graphs from a shell
9
9
10 This extension adds a --graph option to the incoming, outgoing and log
10 This extension adds a --graph option to the incoming, outgoing and log
11 commands. When this options is given, an ASCII representation of the
11 commands. When this options is given, an ASCII representation of the
12 revision graph is also shown.
12 revision graph is also shown.
13 '''
13 '''
14
14
15 from mercurial.cmdutil import revrange, show_changeset
15 from mercurial.cmdutil import revrange, show_changeset
16 from mercurial.commands import templateopts
16 from mercurial.commands import templateopts
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18 from mercurial.node import nullrev
18 from mercurial.node import nullrev
19 from mercurial import cmdutil, commands, extensions
19 from mercurial import cmdutil, commands, extensions
20 from mercurial import hg, scmutil, util, graphmod
20 from mercurial import hg, util, graphmod
21
21
22 ASCIIDATA = 'ASC'
22 ASCIIDATA = 'ASC'
23
23
24 def asciiedges(type, char, lines, seen, rev, parents):
24 def asciiedges(type, char, lines, seen, rev, parents):
25 """adds edge info to changelog DAG walk suitable for ascii()"""
25 """adds edge info to changelog DAG walk suitable for ascii()"""
26 if rev not in seen:
26 if rev not in seen:
27 seen.append(rev)
27 seen.append(rev)
28 nodeidx = seen.index(rev)
28 nodeidx = seen.index(rev)
29
29
30 knownparents = []
30 knownparents = []
31 newparents = []
31 newparents = []
32 for parent in parents:
32 for parent in parents:
33 if parent in seen:
33 if parent in seen:
34 knownparents.append(parent)
34 knownparents.append(parent)
35 else:
35 else:
36 newparents.append(parent)
36 newparents.append(parent)
37
37
38 ncols = len(seen)
38 ncols = len(seen)
39 nextseen = seen[:]
39 nextseen = seen[:]
40 nextseen[nodeidx:nodeidx + 1] = newparents
40 nextseen[nodeidx:nodeidx + 1] = newparents
41 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
41 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
42
42
43 while len(newparents) > 2:
43 while len(newparents) > 2:
44 # ascii() only knows how to add or remove a single column between two
44 # ascii() only knows how to add or remove a single column between two
45 # calls. Nodes with more than two parents break this constraint so we
45 # calls. Nodes with more than two parents break this constraint so we
46 # introduce intermediate expansion lines to grow the active node list
46 # introduce intermediate expansion lines to grow the active node list
47 # slowly.
47 # slowly.
48 edges.append((nodeidx, nodeidx))
48 edges.append((nodeidx, nodeidx))
49 edges.append((nodeidx, nodeidx + 1))
49 edges.append((nodeidx, nodeidx + 1))
50 nmorecols = 1
50 nmorecols = 1
51 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
51 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
52 char = '\\'
52 char = '\\'
53 lines = []
53 lines = []
54 nodeidx += 1
54 nodeidx += 1
55 ncols += 1
55 ncols += 1
56 edges = []
56 edges = []
57 del newparents[0]
57 del newparents[0]
58
58
59 if len(newparents) > 0:
59 if len(newparents) > 0:
60 edges.append((nodeidx, nodeidx))
60 edges.append((nodeidx, nodeidx))
61 if len(newparents) > 1:
61 if len(newparents) > 1:
62 edges.append((nodeidx, nodeidx + 1))
62 edges.append((nodeidx, nodeidx + 1))
63 nmorecols = len(nextseen) - ncols
63 nmorecols = len(nextseen) - ncols
64 seen[:] = nextseen
64 seen[:] = nextseen
65 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
65 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
66
66
67 def fix_long_right_edges(edges):
67 def fix_long_right_edges(edges):
68 for (i, (start, end)) in enumerate(edges):
68 for (i, (start, end)) in enumerate(edges):
69 if end > start:
69 if end > start:
70 edges[i] = (start, end + 1)
70 edges[i] = (start, end + 1)
71
71
72 def get_nodeline_edges_tail(
72 def get_nodeline_edges_tail(
73 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
73 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
74 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
74 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
75 # Still going in the same non-vertical direction.
75 # Still going in the same non-vertical direction.
76 if n_columns_diff == -1:
76 if n_columns_diff == -1:
77 start = max(node_index + 1, p_node_index)
77 start = max(node_index + 1, p_node_index)
78 tail = ["|", " "] * (start - node_index - 1)
78 tail = ["|", " "] * (start - node_index - 1)
79 tail.extend(["/", " "] * (n_columns - start))
79 tail.extend(["/", " "] * (n_columns - start))
80 return tail
80 return tail
81 else:
81 else:
82 return ["\\", " "] * (n_columns - node_index - 1)
82 return ["\\", " "] * (n_columns - node_index - 1)
83 else:
83 else:
84 return ["|", " "] * (n_columns - node_index - 1)
84 return ["|", " "] * (n_columns - node_index - 1)
85
85
86 def draw_edges(edges, nodeline, interline):
86 def draw_edges(edges, nodeline, interline):
87 for (start, end) in edges:
87 for (start, end) in edges:
88 if start == end + 1:
88 if start == end + 1:
89 interline[2 * end + 1] = "/"
89 interline[2 * end + 1] = "/"
90 elif start == end - 1:
90 elif start == end - 1:
91 interline[2 * start + 1] = "\\"
91 interline[2 * start + 1] = "\\"
92 elif start == end:
92 elif start == end:
93 interline[2 * start] = "|"
93 interline[2 * start] = "|"
94 else:
94 else:
95 nodeline[2 * end] = "+"
95 nodeline[2 * end] = "+"
96 if start > end:
96 if start > end:
97 (start, end) = (end, start)
97 (start, end) = (end, start)
98 for i in range(2 * start + 1, 2 * end):
98 for i in range(2 * start + 1, 2 * end):
99 if nodeline[i] != "+":
99 if nodeline[i] != "+":
100 nodeline[i] = "-"
100 nodeline[i] = "-"
101
101
102 def get_padding_line(ni, n_columns, edges):
102 def get_padding_line(ni, n_columns, edges):
103 line = []
103 line = []
104 line.extend(["|", " "] * ni)
104 line.extend(["|", " "] * ni)
105 if (ni, ni - 1) in edges or (ni, ni) in edges:
105 if (ni, ni - 1) in edges or (ni, ni) in edges:
106 # (ni, ni - 1) (ni, ni)
106 # (ni, ni - 1) (ni, ni)
107 # | | | | | | | |
107 # | | | | | | | |
108 # +---o | | o---+
108 # +---o | | o---+
109 # | | c | | c | |
109 # | | c | | c | |
110 # | |/ / | |/ /
110 # | |/ / | |/ /
111 # | | | | | |
111 # | | | | | |
112 c = "|"
112 c = "|"
113 else:
113 else:
114 c = " "
114 c = " "
115 line.extend([c, " "])
115 line.extend([c, " "])
116 line.extend(["|", " "] * (n_columns - ni - 1))
116 line.extend(["|", " "] * (n_columns - ni - 1))
117 return line
117 return line
118
118
119 def asciistate():
119 def asciistate():
120 """returns the initial value for the "state" argument to ascii()"""
120 """returns the initial value for the "state" argument to ascii()"""
121 return [0, 0]
121 return [0, 0]
122
122
123 def ascii(ui, state, type, char, text, coldata):
123 def ascii(ui, state, type, char, text, coldata):
124 """prints an ASCII graph of the DAG
124 """prints an ASCII graph of the DAG
125
125
126 takes the following arguments (one call per node in the graph):
126 takes the following arguments (one call per node in the graph):
127
127
128 - ui to write to
128 - ui to write to
129 - Somewhere to keep the needed state in (init to asciistate())
129 - Somewhere to keep the needed state in (init to asciistate())
130 - Column of the current node in the set of ongoing edges.
130 - Column of the current node in the set of ongoing edges.
131 - Type indicator of node data == ASCIIDATA.
131 - Type indicator of node data == ASCIIDATA.
132 - Payload: (char, lines):
132 - Payload: (char, lines):
133 - Character to use as node's symbol.
133 - Character to use as node's symbol.
134 - List of lines to display as the node's text.
134 - List of lines to display as the node's text.
135 - Edges; a list of (col, next_col) indicating the edges between
135 - Edges; a list of (col, next_col) indicating the edges between
136 the current node and its parents.
136 the current node and its parents.
137 - Number of columns (ongoing edges) in the current revision.
137 - Number of columns (ongoing edges) in the current revision.
138 - The difference between the number of columns (ongoing edges)
138 - The difference between the number of columns (ongoing edges)
139 in the next revision and the number of columns (ongoing edges)
139 in the next revision and the number of columns (ongoing edges)
140 in the current revision. That is: -1 means one column removed;
140 in the current revision. That is: -1 means one column removed;
141 0 means no columns added or removed; 1 means one column added.
141 0 means no columns added or removed; 1 means one column added.
142 """
142 """
143
143
144 idx, edges, ncols, coldiff = coldata
144 idx, edges, ncols, coldiff = coldata
145 assert -2 < coldiff < 2
145 assert -2 < coldiff < 2
146 if coldiff == -1:
146 if coldiff == -1:
147 # Transform
147 # Transform
148 #
148 #
149 # | | | | | |
149 # | | | | | |
150 # o | | into o---+
150 # o | | into o---+
151 # |X / |/ /
151 # |X / |/ /
152 # | | | |
152 # | | | |
153 fix_long_right_edges(edges)
153 fix_long_right_edges(edges)
154
154
155 # add_padding_line says whether to rewrite
155 # add_padding_line says whether to rewrite
156 #
156 #
157 # | | | | | | | |
157 # | | | | | | | |
158 # | o---+ into | o---+
158 # | o---+ into | o---+
159 # | / / | | | # <--- padding line
159 # | / / | | | # <--- padding line
160 # o | | | / /
160 # o | | | / /
161 # o | |
161 # o | |
162 add_padding_line = (len(text) > 2 and coldiff == -1 and
162 add_padding_line = (len(text) > 2 and coldiff == -1 and
163 [x for (x, y) in edges if x + 1 < y])
163 [x for (x, y) in edges if x + 1 < y])
164
164
165 # fix_nodeline_tail says whether to rewrite
165 # fix_nodeline_tail says whether to rewrite
166 #
166 #
167 # | | o | | | | o | |
167 # | | o | | | | o | |
168 # | | |/ / | | |/ /
168 # | | |/ / | | |/ /
169 # | o | | into | o / / # <--- fixed nodeline tail
169 # | o | | into | o / / # <--- fixed nodeline tail
170 # | |/ / | |/ /
170 # | |/ / | |/ /
171 # o | | o | |
171 # o | | o | |
172 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
172 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
173
173
174 # nodeline is the line containing the node character (typically o)
174 # nodeline is the line containing the node character (typically o)
175 nodeline = ["|", " "] * idx
175 nodeline = ["|", " "] * idx
176 nodeline.extend([char, " "])
176 nodeline.extend([char, " "])
177
177
178 nodeline.extend(
178 nodeline.extend(
179 get_nodeline_edges_tail(idx, state[1], ncols, coldiff,
179 get_nodeline_edges_tail(idx, state[1], ncols, coldiff,
180 state[0], fix_nodeline_tail))
180 state[0], fix_nodeline_tail))
181
181
182 # shift_interline is the line containing the non-vertical
182 # shift_interline is the line containing the non-vertical
183 # edges between this entry and the next
183 # edges between this entry and the next
184 shift_interline = ["|", " "] * idx
184 shift_interline = ["|", " "] * idx
185 if coldiff == -1:
185 if coldiff == -1:
186 n_spaces = 1
186 n_spaces = 1
187 edge_ch = "/"
187 edge_ch = "/"
188 elif coldiff == 0:
188 elif coldiff == 0:
189 n_spaces = 2
189 n_spaces = 2
190 edge_ch = "|"
190 edge_ch = "|"
191 else:
191 else:
192 n_spaces = 3
192 n_spaces = 3
193 edge_ch = "\\"
193 edge_ch = "\\"
194 shift_interline.extend(n_spaces * [" "])
194 shift_interline.extend(n_spaces * [" "])
195 shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
195 shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
196
196
197 # draw edges from the current node to its parents
197 # draw edges from the current node to its parents
198 draw_edges(edges, nodeline, shift_interline)
198 draw_edges(edges, nodeline, shift_interline)
199
199
200 # lines is the list of all graph lines to print
200 # lines is the list of all graph lines to print
201 lines = [nodeline]
201 lines = [nodeline]
202 if add_padding_line:
202 if add_padding_line:
203 lines.append(get_padding_line(idx, ncols, edges))
203 lines.append(get_padding_line(idx, ncols, edges))
204 lines.append(shift_interline)
204 lines.append(shift_interline)
205
205
206 # make sure that there are as many graph lines as there are
206 # make sure that there are as many graph lines as there are
207 # log strings
207 # log strings
208 while len(text) < len(lines):
208 while len(text) < len(lines):
209 text.append("")
209 text.append("")
210 if len(lines) < len(text):
210 if len(lines) < len(text):
211 extra_interline = ["|", " "] * (ncols + coldiff)
211 extra_interline = ["|", " "] * (ncols + coldiff)
212 while len(lines) < len(text):
212 while len(lines) < len(text):
213 lines.append(extra_interline)
213 lines.append(extra_interline)
214
214
215 # print lines
215 # print lines
216 indentation_level = max(ncols, ncols + coldiff)
216 indentation_level = max(ncols, ncols + coldiff)
217 for (line, logstr) in zip(lines, text):
217 for (line, logstr) in zip(lines, text):
218 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
218 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
219 ui.write(ln.rstrip() + '\n')
219 ui.write(ln.rstrip() + '\n')
220
220
221 # ... and start over
221 # ... and start over
222 state[0] = coldiff
222 state[0] = coldiff
223 state[1] = idx
223 state[1] = idx
224
224
225 def get_revs(repo, rev_opt):
225 def get_revs(repo, rev_opt):
226 if rev_opt:
226 if rev_opt:
227 revs = revrange(repo, rev_opt)
227 revs = revrange(repo, rev_opt)
228 if len(revs) == 0:
228 if len(revs) == 0:
229 return (nullrev, nullrev)
229 return (nullrev, nullrev)
230 return (max(revs), min(revs))
230 return (max(revs), min(revs))
231 else:
231 else:
232 return (len(repo) - 1, 0)
232 return (len(repo) - 1, 0)
233
233
234 def check_unsupported_flags(pats, opts):
234 def check_unsupported_flags(pats, opts):
235 for op in ["follow_first", "copies", "newest_first"]:
235 for op in ["follow_first", "copies", "newest_first"]:
236 if op in opts and opts[op]:
236 if op in opts and opts[op]:
237 raise util.Abort(_("-G/--graph option is incompatible with --%s")
237 raise util.Abort(_("-G/--graph option is incompatible with --%s")
238 % op.replace("_", "-"))
238 % op.replace("_", "-"))
239 if pats and opts.get('follow'):
239 if pats and opts.get('follow'):
240 raise util.Abort(_("-G/--graph option is incompatible with --follow "
240 raise util.Abort(_("-G/--graph option is incompatible with --follow "
241 "with file argument"))
241 "with file argument"))
242
242
243 def revset(pats, opts):
243 def revset(pats, opts):
244 """Return revset str built of revisions, log options and file patterns.
244 """Return revset str built of revisions, log options and file patterns.
245 """
245 """
246 opt2revset = {
246 opt2revset = {
247 'follow': (0, 'follow()'),
247 'follow': (0, 'follow()'),
248 'no_merges': (0, 'not merge()'),
248 'no_merges': (0, 'not merge()'),
249 'only_merges': (0, 'merge()'),
249 'only_merges': (0, 'merge()'),
250 'removed': (0, 'removes("*")'),
250 'removed': (0, 'removes("*")'),
251 'date': (1, 'date($)'),
251 'date': (1, 'date($)'),
252 'branch': (2, 'branch($)'),
252 'branch': (2, 'branch($)'),
253 'exclude': (2, 'not file($)'),
253 'exclude': (2, 'not file($)'),
254 'include': (2, 'file($)'),
254 'include': (2, 'file($)'),
255 'keyword': (2, 'keyword($)'),
255 'keyword': (2, 'keyword($)'),
256 'only_branch': (2, 'branch($)'),
256 'only_branch': (2, 'branch($)'),
257 'prune': (2, 'not ($ or ancestors($))'),
257 'prune': (2, 'not ($ or ancestors($))'),
258 'user': (2, 'user($)'),
258 'user': (2, 'user($)'),
259 }
259 }
260 optrevset = []
260 optrevset = []
261 revset = []
261 revset = []
262 for op, val in opts.iteritems():
262 for op, val in opts.iteritems():
263 if not val:
263 if not val:
264 continue
264 continue
265 if op == 'rev':
265 if op == 'rev':
266 # Already a revset
266 # Already a revset
267 revset.extend(val)
267 revset.extend(val)
268 if op not in opt2revset:
268 if op not in opt2revset:
269 continue
269 continue
270 arity, revop = opt2revset[op]
270 arity, revop = opt2revset[op]
271 revop = revop.replace('$', '%(val)r')
271 revop = revop.replace('$', '%(val)r')
272 if arity == 0:
272 if arity == 0:
273 optrevset.append(revop)
273 optrevset.append(revop)
274 elif arity == 1:
274 elif arity == 1:
275 optrevset.append(revop % {'val': val})
275 optrevset.append(revop % {'val': val})
276 else:
276 else:
277 for f in val:
277 for f in val:
278 optrevset.append(revop % {'val': f})
278 optrevset.append(revop % {'val': f})
279
279
280 for path in pats:
280 for path in pats:
281 optrevset.append('file(%r)' % path)
281 optrevset.append('file(%r)' % path)
282
282
283 if revset or optrevset:
283 if revset or optrevset:
284 if revset:
284 if revset:
285 revset = ['(' + ' or '.join(revset) + ')']
285 revset = ['(' + ' or '.join(revset) + ')']
286 if optrevset:
286 if optrevset:
287 revset.append('(' + ' and '.join(optrevset) + ')')
287 revset.append('(' + ' and '.join(optrevset) + ')')
288 revset = ' and '.join(revset)
288 revset = ' and '.join(revset)
289 else:
289 else:
290 revset = 'all()'
290 revset = 'all()'
291 return revset
291 return revset
292
292
293 def generate(ui, dag, displayer, showparents, edgefn):
293 def generate(ui, dag, displayer, showparents, edgefn):
294 seen, state = [], asciistate()
294 seen, state = [], asciistate()
295 for rev, type, ctx, parents in dag:
295 for rev, type, ctx, parents in dag:
296 char = ctx.node() in showparents and '@' or 'o'
296 char = ctx.node() in showparents and '@' or 'o'
297 displayer.show(ctx)
297 displayer.show(ctx)
298 lines = displayer.hunk.pop(rev).split('\n')[:-1]
298 lines = displayer.hunk.pop(rev).split('\n')[:-1]
299 displayer.flush(rev)
299 displayer.flush(rev)
300 edges = edgefn(type, char, lines, seen, rev, parents)
300 edges = edgefn(type, char, lines, seen, rev, parents)
301 for type, char, lines, coldata in edges:
301 for type, char, lines, coldata in edges:
302 ascii(ui, state, type, char, lines, coldata)
302 ascii(ui, state, type, char, lines, coldata)
303 displayer.close()
303 displayer.close()
304
304
305 def graphlog(ui, repo, *pats, **opts):
305 def graphlog(ui, repo, *pats, **opts):
306 """show revision history alongside an ASCII revision graph
306 """show revision history alongside an ASCII revision graph
307
307
308 Print a revision history alongside a revision graph drawn with
308 Print a revision history alongside a revision graph drawn with
309 ASCII characters.
309 ASCII characters.
310
310
311 Nodes printed as an @ character are parents of the working
311 Nodes printed as an @ character are parents of the working
312 directory.
312 directory.
313 """
313 """
314
314
315 check_unsupported_flags(pats, opts)
315 check_unsupported_flags(pats, opts)
316
316
317 revs = sorted(revrange(repo, [revset(pats, opts)]), reverse=1)
317 revs = sorted(revrange(repo, [revset(pats, opts)]), reverse=1)
318 limit = cmdutil.loglimit(opts)
318 limit = cmdutil.loglimit(opts)
319 if limit is not None:
319 if limit is not None:
320 revs = revs[:limit]
320 revs = revs[:limit]
321 revdag = graphmod.dagwalker(repo, revs)
321 revdag = graphmod.dagwalker(repo, revs)
322
322
323 displayer = show_changeset(ui, repo, opts, buffered=True)
323 displayer = show_changeset(ui, repo, opts, buffered=True)
324 showparents = [ctx.node() for ctx in repo[None].parents()]
324 showparents = [ctx.node() for ctx in repo[None].parents()]
325 generate(ui, revdag, displayer, showparents, asciiedges)
325 generate(ui, revdag, displayer, showparents, asciiedges)
326
326
327 def graphrevs(repo, nodes, opts):
327 def graphrevs(repo, nodes, opts):
328 limit = cmdutil.loglimit(opts)
328 limit = cmdutil.loglimit(opts)
329 nodes.reverse()
329 nodes.reverse()
330 if limit is not None:
330 if limit is not None:
331 nodes = nodes[:limit]
331 nodes = nodes[:limit]
332 return graphmod.nodes(repo, nodes)
332 return graphmod.nodes(repo, nodes)
333
333
334 def goutgoing(ui, repo, dest=None, **opts):
334 def goutgoing(ui, repo, dest=None, **opts):
335 """show the outgoing changesets alongside an ASCII revision graph
335 """show the outgoing changesets alongside an ASCII revision graph
336
336
337 Print the outgoing changesets alongside a revision graph drawn with
337 Print the outgoing changesets alongside a revision graph drawn with
338 ASCII characters.
338 ASCII characters.
339
339
340 Nodes printed as an @ character are parents of the working
340 Nodes printed as an @ character are parents of the working
341 directory.
341 directory.
342 """
342 """
343
343
344 check_unsupported_flags([], opts)
344 check_unsupported_flags([], opts)
345 o = hg._outgoing(ui, repo, dest, opts)
345 o = hg._outgoing(ui, repo, dest, opts)
346 if o is None:
346 if o is None:
347 return
347 return
348
348
349 revdag = graphrevs(repo, o, opts)
349 revdag = graphrevs(repo, o, opts)
350 displayer = show_changeset(ui, repo, opts, buffered=True)
350 displayer = show_changeset(ui, repo, opts, buffered=True)
351 showparents = [ctx.node() for ctx in repo[None].parents()]
351 showparents = [ctx.node() for ctx in repo[None].parents()]
352 generate(ui, revdag, displayer, showparents, asciiedges)
352 generate(ui, revdag, displayer, showparents, asciiedges)
353
353
354 def gincoming(ui, repo, source="default", **opts):
354 def gincoming(ui, repo, source="default", **opts):
355 """show the incoming changesets alongside an ASCII revision graph
355 """show the incoming changesets alongside an ASCII revision graph
356
356
357 Print the incoming changesets alongside a revision graph drawn with
357 Print the incoming changesets alongside a revision graph drawn with
358 ASCII characters.
358 ASCII characters.
359
359
360 Nodes printed as an @ character are parents of the working
360 Nodes printed as an @ character are parents of the working
361 directory.
361 directory.
362 """
362 """
363 def subreporecurse():
363 def subreporecurse():
364 return 1
364 return 1
365
365
366 check_unsupported_flags([], opts)
366 check_unsupported_flags([], opts)
367 def display(other, chlist, displayer):
367 def display(other, chlist, displayer):
368 revdag = graphrevs(other, chlist, opts)
368 revdag = graphrevs(other, chlist, opts)
369 showparents = [ctx.node() for ctx in repo[None].parents()]
369 showparents = [ctx.node() for ctx in repo[None].parents()]
370 generate(ui, revdag, displayer, showparents, asciiedges)
370 generate(ui, revdag, displayer, showparents, asciiedges)
371
371
372 hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
372 hg._incoming(display, subreporecurse, ui, repo, source, opts, buffered=True)
373
373
374 def uisetup(ui):
374 def uisetup(ui):
375 '''Initialize the extension.'''
375 '''Initialize the extension.'''
376 _wrapcmd(ui, 'log', commands.table, graphlog)
376 _wrapcmd(ui, 'log', commands.table, graphlog)
377 _wrapcmd(ui, 'incoming', commands.table, gincoming)
377 _wrapcmd(ui, 'incoming', commands.table, gincoming)
378 _wrapcmd(ui, 'outgoing', commands.table, goutgoing)
378 _wrapcmd(ui, 'outgoing', commands.table, goutgoing)
379
379
380 def _wrapcmd(ui, cmd, table, wrapfn):
380 def _wrapcmd(ui, cmd, table, wrapfn):
381 '''wrap the command'''
381 '''wrap the command'''
382 def graph(orig, *args, **kwargs):
382 def graph(orig, *args, **kwargs):
383 if kwargs['graph']:
383 if kwargs['graph']:
384 return wrapfn(*args, **kwargs)
384 return wrapfn(*args, **kwargs)
385 return orig(*args, **kwargs)
385 return orig(*args, **kwargs)
386 entry = extensions.wrapcommand(table, cmd, graph)
386 entry = extensions.wrapcommand(table, cmd, graph)
387 entry[1].append(('G', 'graph', None, _("show the revision DAG")))
387 entry[1].append(('G', 'graph', None, _("show the revision DAG")))
388
388
389 cmdtable = {
389 cmdtable = {
390 "glog":
390 "glog":
391 (graphlog,
391 (graphlog,
392 [('l', 'limit', '',
392 [('l', 'limit', '',
393 _('limit number of changes displayed'), _('NUM')),
393 _('limit number of changes displayed'), _('NUM')),
394 ('p', 'patch', False, _('show patch')),
394 ('p', 'patch', False, _('show patch')),
395 ('r', 'rev', [],
395 ('r', 'rev', [],
396 _('show the specified revision or range'), _('REV')),
396 _('show the specified revision or range'), _('REV')),
397 ] + templateopts,
397 ] + templateopts,
398 _('hg glog [OPTION]... [FILE]')),
398 _('hg glog [OPTION]... [FILE]')),
399 }
399 }
@@ -1,275 +1,274
1 # progress.py show progress bars for some actions
1 # progress.py show progress bars for some actions
2 #
2 #
3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
4 #
4 #
5 # This program is free software; you can redistribute it and/or modify it
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
8 # option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful, but
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 # Public License for more details.
13 # Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License along
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
18
19 """show progress bars for some actions
19 """show progress bars for some actions
20
20
21 This extension uses the progress information logged by hg commands
21 This extension uses the progress information logged by hg commands
22 to draw progress bars that are as informative as possible. Some progress
22 to draw progress bars that are as informative as possible. Some progress
23 bars only offer indeterminate information, while others have a definite
23 bars only offer indeterminate information, while others have a definite
24 end point.
24 end point.
25
25
26 The following settings are available::
26 The following settings are available::
27
27
28 [progress]
28 [progress]
29 delay = 3 # number of seconds (float) before showing the progress bar
29 delay = 3 # number of seconds (float) before showing the progress bar
30 refresh = 0.1 # time in seconds between refreshes of the progress bar
30 refresh = 0.1 # time in seconds between refreshes of the progress bar
31 format = topic bar number estimate # format of the progress bar
31 format = topic bar number estimate # format of the progress bar
32 width = <none> # if set, the maximum width of the progress information
32 width = <none> # if set, the maximum width of the progress information
33 # (that is, min(width, term width) will be used)
33 # (that is, min(width, term width) will be used)
34 clear-complete = True # clear the progress bar after it's done
34 clear-complete = True # clear the progress bar after it's done
35 disable = False # if true, don't show a progress bar
35 disable = False # if true, don't show a progress bar
36 assume-tty = False # if true, ALWAYS show a progress bar, unless
36 assume-tty = False # if true, ALWAYS show a progress bar, unless
37 # disable is given
37 # disable is given
38
38
39 Valid entries for the format field are topic, bar, number, unit,
39 Valid entries for the format field are topic, bar, number, unit,
40 estimate, and item. item defaults to the last 20 characters of the
40 estimate, and item. item defaults to the last 20 characters of the
41 item, but this can be changed by adding either ``-<num>`` which would
41 item, but this can be changed by adding either ``-<num>`` which would
42 take the last num characters, or ``+<num>`` for the first num
42 take the last num characters, or ``+<num>`` for the first num
43 characters.
43 characters.
44 """
44 """
45
45
46 import sys
46 import sys
47 import time
47 import time
48
48
49 from mercurial.i18n import _
49 from mercurial.i18n import _
50 from mercurial import util
51
50
52 def spacejoin(*args):
51 def spacejoin(*args):
53 return ' '.join(s for s in args if s)
52 return ' '.join(s for s in args if s)
54
53
55 def shouldprint(ui):
54 def shouldprint(ui):
56 return (getattr(sys.stderr, 'isatty', None) and
55 return (getattr(sys.stderr, 'isatty', None) and
57 (sys.stderr.isatty() or ui.configbool('progress', 'assume-tty')))
56 (sys.stderr.isatty() or ui.configbool('progress', 'assume-tty')))
58
57
59 def fmtremaining(seconds):
58 def fmtremaining(seconds):
60 if seconds < 60:
59 if seconds < 60:
61 # i18n: format XX seconds as "XXs"
60 # i18n: format XX seconds as "XXs"
62 return _("%02ds") % (seconds)
61 return _("%02ds") % (seconds)
63 minutes = seconds // 60
62 minutes = seconds // 60
64 if minutes < 60:
63 if minutes < 60:
65 seconds -= minutes * 60
64 seconds -= minutes * 60
66 # i18n: format X minutes and YY seconds as "XmYYs"
65 # i18n: format X minutes and YY seconds as "XmYYs"
67 return _("%dm%02ds") % (minutes, seconds)
66 return _("%dm%02ds") % (minutes, seconds)
68 # we're going to ignore seconds in this case
67 # we're going to ignore seconds in this case
69 minutes += 1
68 minutes += 1
70 hours = minutes // 60
69 hours = minutes // 60
71 minutes -= hours * 60
70 minutes -= hours * 60
72 if hours < 30:
71 if hours < 30:
73 # i18n: format X hours and YY minutes as "XhYYm"
72 # i18n: format X hours and YY minutes as "XhYYm"
74 return _("%dh%02dm") % (hours, minutes)
73 return _("%dh%02dm") % (hours, minutes)
75 # we're going to ignore minutes in this case
74 # we're going to ignore minutes in this case
76 hours += 1
75 hours += 1
77 days = hours // 24
76 days = hours // 24
78 hours -= days * 24
77 hours -= days * 24
79 if days < 15:
78 if days < 15:
80 # i18n: format X days and YY hours as "XdYYh"
79 # i18n: format X days and YY hours as "XdYYh"
81 return _("%dd%02dh") % (days, hours)
80 return _("%dd%02dh") % (days, hours)
82 # we're going to ignore hours in this case
81 # we're going to ignore hours in this case
83 days += 1
82 days += 1
84 weeks = days // 7
83 weeks = days // 7
85 days -= weeks * 7
84 days -= weeks * 7
86 if weeks < 55:
85 if weeks < 55:
87 # i18n: format X weeks and YY days as "XwYYd"
86 # i18n: format X weeks and YY days as "XwYYd"
88 return _("%dw%02dd") % (weeks, days)
87 return _("%dw%02dd") % (weeks, days)
89 # we're going to ignore days and treat a year as 52 weeks
88 # we're going to ignore days and treat a year as 52 weeks
90 weeks += 1
89 weeks += 1
91 years = weeks // 52
90 years = weeks // 52
92 weeks -= years * 52
91 weeks -= years * 52
93 # i18n: format X years and YY weeks as "XyYYw"
92 # i18n: format X years and YY weeks as "XyYYw"
94 return _("%dy%02dw") % (years, weeks)
93 return _("%dy%02dw") % (years, weeks)
95
94
96 class progbar(object):
95 class progbar(object):
97 def __init__(self, ui):
96 def __init__(self, ui):
98 self.ui = ui
97 self.ui = ui
99 self.resetstate()
98 self.resetstate()
100
99
101 def resetstate(self):
100 def resetstate(self):
102 self.topics = []
101 self.topics = []
103 self.topicstates = {}
102 self.topicstates = {}
104 self.starttimes = {}
103 self.starttimes = {}
105 self.startvals = {}
104 self.startvals = {}
106 self.printed = False
105 self.printed = False
107 self.lastprint = time.time() + float(self.ui.config(
106 self.lastprint = time.time() + float(self.ui.config(
108 'progress', 'delay', default=3))
107 'progress', 'delay', default=3))
109 self.indetcount = 0
108 self.indetcount = 0
110 self.refresh = float(self.ui.config(
109 self.refresh = float(self.ui.config(
111 'progress', 'refresh', default=0.1))
110 'progress', 'refresh', default=0.1))
112 self.order = self.ui.configlist(
111 self.order = self.ui.configlist(
113 'progress', 'format',
112 'progress', 'format',
114 default=['topic', 'bar', 'number', 'estimate'])
113 default=['topic', 'bar', 'number', 'estimate'])
115
114
116 def show(self, now, topic, pos, item, unit, total):
115 def show(self, now, topic, pos, item, unit, total):
117 if not shouldprint(self.ui):
116 if not shouldprint(self.ui):
118 return
117 return
119 termwidth = self.width()
118 termwidth = self.width()
120 self.printed = True
119 self.printed = True
121 head = ''
120 head = ''
122 needprogress = False
121 needprogress = False
123 tail = ''
122 tail = ''
124 for indicator in self.order:
123 for indicator in self.order:
125 add = ''
124 add = ''
126 if indicator == 'topic':
125 if indicator == 'topic':
127 add = topic
126 add = topic
128 elif indicator == 'number':
127 elif indicator == 'number':
129 if total:
128 if total:
130 add = ('% ' + str(len(str(total))) +
129 add = ('% ' + str(len(str(total))) +
131 's/%s') % (pos, total)
130 's/%s') % (pos, total)
132 else:
131 else:
133 add = str(pos)
132 add = str(pos)
134 elif indicator.startswith('item') and item:
133 elif indicator.startswith('item') and item:
135 slice = 'end'
134 slice = 'end'
136 if '-' in indicator:
135 if '-' in indicator:
137 wid = int(indicator.split('-')[1])
136 wid = int(indicator.split('-')[1])
138 elif '+' in indicator:
137 elif '+' in indicator:
139 slice = 'beginning'
138 slice = 'beginning'
140 wid = int(indicator.split('+')[1])
139 wid = int(indicator.split('+')[1])
141 else:
140 else:
142 wid = 20
141 wid = 20
143 if slice == 'end':
142 if slice == 'end':
144 add = item[-wid:]
143 add = item[-wid:]
145 else:
144 else:
146 add = item[:wid]
145 add = item[:wid]
147 add += (wid - len(add)) * ' '
146 add += (wid - len(add)) * ' '
148 elif indicator == 'bar':
147 elif indicator == 'bar':
149 add = ''
148 add = ''
150 needprogress = True
149 needprogress = True
151 elif indicator == 'unit' and unit:
150 elif indicator == 'unit' and unit:
152 add = unit
151 add = unit
153 elif indicator == 'estimate':
152 elif indicator == 'estimate':
154 add = self.estimate(topic, pos, total, now)
153 add = self.estimate(topic, pos, total, now)
155 if not needprogress:
154 if not needprogress:
156 head = spacejoin(head, add)
155 head = spacejoin(head, add)
157 else:
156 else:
158 tail = spacejoin(tail, add)
157 tail = spacejoin(tail, add)
159 if needprogress:
158 if needprogress:
160 used = 0
159 used = 0
161 if head:
160 if head:
162 used += len(head) + 1
161 used += len(head) + 1
163 if tail:
162 if tail:
164 used += len(tail) + 1
163 used += len(tail) + 1
165 progwidth = termwidth - used - 3
164 progwidth = termwidth - used - 3
166 if total and pos <= total:
165 if total and pos <= total:
167 amt = pos * progwidth // total
166 amt = pos * progwidth // total
168 bar = '=' * (amt - 1)
167 bar = '=' * (amt - 1)
169 if amt > 0:
168 if amt > 0:
170 bar += '>'
169 bar += '>'
171 bar += ' ' * (progwidth - amt)
170 bar += ' ' * (progwidth - amt)
172 else:
171 else:
173 progwidth -= 3
172 progwidth -= 3
174 self.indetcount += 1
173 self.indetcount += 1
175 # mod the count by twice the width so we can make the
174 # mod the count by twice the width so we can make the
176 # cursor bounce between the right and left sides
175 # cursor bounce between the right and left sides
177 amt = self.indetcount % (2 * progwidth)
176 amt = self.indetcount % (2 * progwidth)
178 amt -= progwidth
177 amt -= progwidth
179 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
178 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
180 ' ' * int(abs(amt)))
179 ' ' * int(abs(amt)))
181 prog = ''.join(('[', bar , ']'))
180 prog = ''.join(('[', bar , ']'))
182 out = spacejoin(head, prog, tail)
181 out = spacejoin(head, prog, tail)
183 else:
182 else:
184 out = spacejoin(head, tail)
183 out = spacejoin(head, tail)
185 sys.stderr.write('\r' + out[:termwidth])
184 sys.stderr.write('\r' + out[:termwidth])
186 sys.stderr.flush()
185 sys.stderr.flush()
187
186
188 def clear(self):
187 def clear(self):
189 if not shouldprint(self.ui):
188 if not shouldprint(self.ui):
190 return
189 return
191 sys.stderr.write('\r%s\r' % (' ' * self.width()))
190 sys.stderr.write('\r%s\r' % (' ' * self.width()))
192
191
193 def complete(self):
192 def complete(self):
194 if not shouldprint(self.ui):
193 if not shouldprint(self.ui):
195 return
194 return
196 if self.ui.configbool('progress', 'clear-complete', default=True):
195 if self.ui.configbool('progress', 'clear-complete', default=True):
197 self.clear()
196 self.clear()
198 else:
197 else:
199 sys.stderr.write('\n')
198 sys.stderr.write('\n')
200 sys.stderr.flush()
199 sys.stderr.flush()
201
200
202 def width(self):
201 def width(self):
203 tw = self.ui.termwidth()
202 tw = self.ui.termwidth()
204 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
203 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
205
204
206 def estimate(self, topic, pos, total, now):
205 def estimate(self, topic, pos, total, now):
207 if total is None:
206 if total is None:
208 return ''
207 return ''
209 initialpos = self.startvals[topic]
208 initialpos = self.startvals[topic]
210 target = total - initialpos
209 target = total - initialpos
211 delta = pos - initialpos
210 delta = pos - initialpos
212 if delta > 0:
211 if delta > 0:
213 elapsed = now - self.starttimes[topic]
212 elapsed = now - self.starttimes[topic]
214 if elapsed > float(
213 if elapsed > float(
215 self.ui.config('progress', 'estimate', default=2)):
214 self.ui.config('progress', 'estimate', default=2)):
216 seconds = (elapsed * (target - delta)) // delta + 1
215 seconds = (elapsed * (target - delta)) // delta + 1
217 return fmtremaining(seconds)
216 return fmtremaining(seconds)
218 return ''
217 return ''
219
218
220 def progress(self, topic, pos, item='', unit='', total=None):
219 def progress(self, topic, pos, item='', unit='', total=None):
221 now = time.time()
220 now = time.time()
222 if pos is None:
221 if pos is None:
223 self.starttimes.pop(topic, None)
222 self.starttimes.pop(topic, None)
224 self.startvals.pop(topic, None)
223 self.startvals.pop(topic, None)
225 self.topicstates.pop(topic, None)
224 self.topicstates.pop(topic, None)
226 # reset the progress bar if this is the outermost topic
225 # reset the progress bar if this is the outermost topic
227 if self.topics and self.topics[0] == topic and self.printed:
226 if self.topics and self.topics[0] == topic and self.printed:
228 self.complete()
227 self.complete()
229 self.resetstate()
228 self.resetstate()
230 # truncate the list of topics assuming all topics within
229 # truncate the list of topics assuming all topics within
231 # this one are also closed
230 # this one are also closed
232 if topic in self.topics:
231 if topic in self.topics:
233 self.topics = self.topics[:self.topics.index(topic)]
232 self.topics = self.topics[:self.topics.index(topic)]
234 else:
233 else:
235 if topic not in self.topics:
234 if topic not in self.topics:
236 self.starttimes[topic] = now
235 self.starttimes[topic] = now
237 self.startvals[topic] = pos
236 self.startvals[topic] = pos
238 self.topics.append(topic)
237 self.topics.append(topic)
239 self.topicstates[topic] = pos, item, unit, total
238 self.topicstates[topic] = pos, item, unit, total
240 if now - self.lastprint >= self.refresh and self.topics:
239 if now - self.lastprint >= self.refresh and self.topics:
241 self.lastprint = now
240 self.lastprint = now
242 current = self.topics[-1]
241 current = self.topics[-1]
243 self.show(now, topic, *self.topicstates[topic])
242 self.show(now, topic, *self.topicstates[topic])
244
243
245 def uisetup(ui):
244 def uisetup(ui):
246 class progressui(ui.__class__):
245 class progressui(ui.__class__):
247 _progbar = None
246 _progbar = None
248
247
249 def progress(self, *args, **opts):
248 def progress(self, *args, **opts):
250 self._progbar.progress(*args, **opts)
249 self._progbar.progress(*args, **opts)
251 return super(progressui, self).progress(*args, **opts)
250 return super(progressui, self).progress(*args, **opts)
252
251
253 def write(self, *args, **opts):
252 def write(self, *args, **opts):
254 if self._progbar.printed:
253 if self._progbar.printed:
255 self._progbar.clear()
254 self._progbar.clear()
256 return super(progressui, self).write(*args, **opts)
255 return super(progressui, self).write(*args, **opts)
257
256
258 def write_err(self, *args, **opts):
257 def write_err(self, *args, **opts):
259 if self._progbar.printed:
258 if self._progbar.printed:
260 self._progbar.clear()
259 self._progbar.clear()
261 return super(progressui, self).write_err(*args, **opts)
260 return super(progressui, self).write_err(*args, **opts)
262
261
263 # Apps that derive a class from ui.ui() can use
262 # Apps that derive a class from ui.ui() can use
264 # setconfig('progress', 'disable', 'True') to disable this extension
263 # setconfig('progress', 'disable', 'True') to disable this extension
265 if ui.configbool('progress', 'disable'):
264 if ui.configbool('progress', 'disable'):
266 return
265 return
267 if shouldprint(ui) and not ui.debugflag and not ui.quiet:
266 if shouldprint(ui) and not ui.debugflag and not ui.quiet:
268 ui.__class__ = progressui
267 ui.__class__ = progressui
269 # we instantiate one globally shared progress bar to avoid
268 # we instantiate one globally shared progress bar to avoid
270 # competing progress bars when multiple UI objects get created
269 # competing progress bars when multiple UI objects get created
271 if not progressui._progbar:
270 if not progressui._progbar:
272 progressui._progbar = progbar(ui)
271 progressui._progbar = progbar(ui)
273
272
274 def reposetup(ui, repo):
273 def reposetup(ui, repo):
275 uisetup(repo.ui)
274 uisetup(repo.ui)
General Comments 0
You need to be logged in to leave comments. Login now