##// END OF EJS Templates
summary: add a histedit hook
Bryan O'Sullivan -
r19215:f184fe1e default
parent child Browse files
Show More
@@ -1,550 +1,553 b''
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 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 '''colorize output from some commands
8 '''colorize output from some commands
9
9
10 This extension modifies the status and resolve commands to add color
10 This extension modifies the status and resolve commands to add color
11 to their output to reflect file status, the qseries command to add
11 to their output to reflect file status, the qseries command to add
12 color to reflect patch status (applied, unapplied, missing), and to
12 color to reflect patch status (applied, unapplied, missing), and to
13 diff-related commands to highlight additions, removals, diff headers,
13 diff-related commands to highlight additions, removals, diff headers,
14 and trailing whitespace.
14 and trailing whitespace.
15
15
16 Other effects in addition to color, like bold and underlined text, are
16 Other effects in addition to color, like bold and underlined text, are
17 also available. By default, the terminfo database is used to find the
17 also available. By default, the terminfo database is used to find the
18 terminal codes used to change color and effect. If terminfo is not
18 terminal codes used to change color and effect. If terminfo is not
19 available, then effects are rendered with the ECMA-48 SGR control
19 available, then effects are rendered with the ECMA-48 SGR control
20 function (aka ANSI escape codes).
20 function (aka ANSI escape codes).
21
21
22 Default effects may be overridden from your configuration file::
22 Default effects may be overridden from your configuration file::
23
23
24 [color]
24 [color]
25 status.modified = blue bold underline red_background
25 status.modified = blue bold underline red_background
26 status.added = green bold
26 status.added = green bold
27 status.removed = red bold blue_background
27 status.removed = red bold blue_background
28 status.deleted = cyan bold underline
28 status.deleted = cyan bold underline
29 status.unknown = magenta bold underline
29 status.unknown = magenta bold underline
30 status.ignored = black bold
30 status.ignored = black bold
31
31
32 # 'none' turns off all effects
32 # 'none' turns off all effects
33 status.clean = none
33 status.clean = none
34 status.copied = none
34 status.copied = none
35
35
36 qseries.applied = blue bold underline
36 qseries.applied = blue bold underline
37 qseries.unapplied = black bold
37 qseries.unapplied = black bold
38 qseries.missing = red bold
38 qseries.missing = red bold
39
39
40 diff.diffline = bold
40 diff.diffline = bold
41 diff.extended = cyan bold
41 diff.extended = cyan bold
42 diff.file_a = red bold
42 diff.file_a = red bold
43 diff.file_b = green bold
43 diff.file_b = green bold
44 diff.hunk = magenta
44 diff.hunk = magenta
45 diff.deleted = red
45 diff.deleted = red
46 diff.inserted = green
46 diff.inserted = green
47 diff.changed = white
47 diff.changed = white
48 diff.trailingwhitespace = bold red_background
48 diff.trailingwhitespace = bold red_background
49
49
50 resolve.unresolved = red bold
50 resolve.unresolved = red bold
51 resolve.resolved = green bold
51 resolve.resolved = green bold
52
52
53 bookmarks.current = green
53 bookmarks.current = green
54
54
55 branches.active = none
55 branches.active = none
56 branches.closed = black bold
56 branches.closed = black bold
57 branches.current = green
57 branches.current = green
58 branches.inactive = none
58 branches.inactive = none
59
59
60 tags.normal = green
60 tags.normal = green
61 tags.local = black bold
61 tags.local = black bold
62
62
63 rebase.rebased = blue
63 rebase.rebased = blue
64 rebase.remaining = red bold
64 rebase.remaining = red bold
65
65
66 histedit.remaining = red bold
67
66 The available effects in terminfo mode are 'blink', 'bold', 'dim',
68 The available effects in terminfo mode are 'blink', 'bold', 'dim',
67 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
69 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
68 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
70 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
69 'underline'. How each is rendered depends on the terminal emulator.
71 'underline'. How each is rendered depends on the terminal emulator.
70 Some may not be available for a given terminal type, and will be
72 Some may not be available for a given terminal type, and will be
71 silently ignored.
73 silently ignored.
72
74
73 Note that on some systems, terminfo mode may cause problems when using
75 Note that on some systems, terminfo mode may cause problems when using
74 color with the pager extension and less -R. less with the -R option
76 color with the pager extension and less -R. less with the -R option
75 will only display ECMA-48 color codes, and terminfo mode may sometimes
77 will only display ECMA-48 color codes, and terminfo mode may sometimes
76 emit codes that less doesn't understand. You can work around this by
78 emit codes that less doesn't understand. You can work around this by
77 either using ansi mode (or auto mode), or by using less -r (which will
79 either using ansi mode (or auto mode), or by using less -r (which will
78 pass through all terminal control codes, not just color control
80 pass through all terminal control codes, not just color control
79 codes).
81 codes).
80
82
81 Because there are only eight standard colors, this module allows you
83 Because there are only eight standard colors, this module allows you
82 to define color names for other color slots which might be available
84 to define color names for other color slots which might be available
83 for your terminal type, assuming terminfo mode. For instance::
85 for your terminal type, assuming terminfo mode. For instance::
84
86
85 color.brightblue = 12
87 color.brightblue = 12
86 color.pink = 207
88 color.pink = 207
87 color.orange = 202
89 color.orange = 202
88
90
89 to set 'brightblue' to color slot 12 (useful for 16 color terminals
91 to set 'brightblue' to color slot 12 (useful for 16 color terminals
90 that have brighter colors defined in the upper eight) and, 'pink' and
92 that have brighter colors defined in the upper eight) and, 'pink' and
91 'orange' to colors in 256-color xterm's default color cube. These
93 'orange' to colors in 256-color xterm's default color cube. These
92 defined colors may then be used as any of the pre-defined eight,
94 defined colors may then be used as any of the pre-defined eight,
93 including appending '_background' to set the background to that color.
95 including appending '_background' to set the background to that color.
94
96
95 By default, the color extension will use ANSI mode (or win32 mode on
97 By default, the color extension will use ANSI mode (or win32 mode on
96 Windows) if it detects a terminal. To override auto mode (to enable
98 Windows) if it detects a terminal. To override auto mode (to enable
97 terminfo mode, for example), set the following configuration option::
99 terminfo mode, for example), set the following configuration option::
98
100
99 [color]
101 [color]
100 mode = terminfo
102 mode = terminfo
101
103
102 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
104 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
103 disable color.
105 disable color.
104 '''
106 '''
105
107
106 import os
108 import os
107
109
108 from mercurial import commands, dispatch, extensions, ui as uimod, util
110 from mercurial import commands, dispatch, extensions, ui as uimod, util
109 from mercurial import templater, error
111 from mercurial import templater, error
110 from mercurial.i18n import _
112 from mercurial.i18n import _
111
113
112 testedwith = 'internal'
114 testedwith = 'internal'
113
115
114 # start and stop parameters for effects
116 # start and stop parameters for effects
115 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
117 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
116 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
118 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
117 'italic': 3, 'underline': 4, 'inverse': 7,
119 'italic': 3, 'underline': 4, 'inverse': 7,
118 'black_background': 40, 'red_background': 41,
120 'black_background': 40, 'red_background': 41,
119 'green_background': 42, 'yellow_background': 43,
121 'green_background': 42, 'yellow_background': 43,
120 'blue_background': 44, 'purple_background': 45,
122 'blue_background': 44, 'purple_background': 45,
121 'cyan_background': 46, 'white_background': 47}
123 'cyan_background': 46, 'white_background': 47}
122
124
123 def _terminfosetup(ui, mode):
125 def _terminfosetup(ui, mode):
124 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
126 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
125
127
126 global _terminfo_params
128 global _terminfo_params
127 # If we failed to load curses, we go ahead and return.
129 # If we failed to load curses, we go ahead and return.
128 if not _terminfo_params:
130 if not _terminfo_params:
129 return
131 return
130 # Otherwise, see what the config file says.
132 # Otherwise, see what the config file says.
131 if mode not in ('auto', 'terminfo'):
133 if mode not in ('auto', 'terminfo'):
132 return
134 return
133
135
134 _terminfo_params.update((key[6:], (False, int(val)))
136 _terminfo_params.update((key[6:], (False, int(val)))
135 for key, val in ui.configitems('color')
137 for key, val in ui.configitems('color')
136 if key.startswith('color.'))
138 if key.startswith('color.'))
137
139
138 try:
140 try:
139 curses.setupterm()
141 curses.setupterm()
140 except curses.error, e:
142 except curses.error, e:
141 _terminfo_params = {}
143 _terminfo_params = {}
142 return
144 return
143
145
144 for key, (b, e) in _terminfo_params.items():
146 for key, (b, e) in _terminfo_params.items():
145 if not b:
147 if not b:
146 continue
148 continue
147 if not curses.tigetstr(e):
149 if not curses.tigetstr(e):
148 # Most terminals don't support dim, invis, etc, so don't be
150 # Most terminals don't support dim, invis, etc, so don't be
149 # noisy and use ui.debug().
151 # noisy and use ui.debug().
150 ui.debug("no terminfo entry for %s\n" % e)
152 ui.debug("no terminfo entry for %s\n" % e)
151 del _terminfo_params[key]
153 del _terminfo_params[key]
152 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
154 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
153 # Only warn about missing terminfo entries if we explicitly asked for
155 # Only warn about missing terminfo entries if we explicitly asked for
154 # terminfo mode.
156 # terminfo mode.
155 if mode == "terminfo":
157 if mode == "terminfo":
156 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
158 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
157 "ECMA-48 color\n"))
159 "ECMA-48 color\n"))
158 _terminfo_params = {}
160 _terminfo_params = {}
159
161
160 def _modesetup(ui, opts):
162 def _modesetup(ui, opts):
161 global _terminfo_params
163 global _terminfo_params
162
164
163 coloropt = opts['color']
165 coloropt = opts['color']
164 auto = coloropt == 'auto'
166 auto = coloropt == 'auto'
165 always = not auto and util.parsebool(coloropt)
167 always = not auto and util.parsebool(coloropt)
166 if not always and not auto:
168 if not always and not auto:
167 return None
169 return None
168
170
169 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
171 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
170
172
171 mode = ui.config('color', 'mode', 'auto')
173 mode = ui.config('color', 'mode', 'auto')
172 realmode = mode
174 realmode = mode
173 if mode == 'auto':
175 if mode == 'auto':
174 if os.name == 'nt' and 'TERM' not in os.environ:
176 if os.name == 'nt' and 'TERM' not in os.environ:
175 # looks line a cmd.exe console, use win32 API or nothing
177 # looks line a cmd.exe console, use win32 API or nothing
176 realmode = 'win32'
178 realmode = 'win32'
177 else:
179 else:
178 realmode = 'ansi'
180 realmode = 'ansi'
179
181
180 if realmode == 'win32':
182 if realmode == 'win32':
181 _terminfo_params = {}
183 _terminfo_params = {}
182 if not w32effects:
184 if not w32effects:
183 if mode == 'win32':
185 if mode == 'win32':
184 # only warn if color.mode is explicitly set to win32
186 # only warn if color.mode is explicitly set to win32
185 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
187 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
186 return None
188 return None
187 _effects.update(w32effects)
189 _effects.update(w32effects)
188 elif realmode == 'ansi':
190 elif realmode == 'ansi':
189 _terminfo_params = {}
191 _terminfo_params = {}
190 elif realmode == 'terminfo':
192 elif realmode == 'terminfo':
191 _terminfosetup(ui, mode)
193 _terminfosetup(ui, mode)
192 if not _terminfo_params:
194 if not _terminfo_params:
193 if mode == 'terminfo':
195 if mode == 'terminfo':
194 ## FIXME Shouldn't we return None in this case too?
196 ## FIXME Shouldn't we return None in this case too?
195 # only warn if color.mode is explicitly set to win32
197 # only warn if color.mode is explicitly set to win32
196 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
198 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
197 realmode = 'ansi'
199 realmode = 'ansi'
198 else:
200 else:
199 return None
201 return None
200
202
201 if always or (auto and formatted):
203 if always or (auto and formatted):
202 return realmode
204 return realmode
203 return None
205 return None
204
206
205 try:
207 try:
206 import curses
208 import curses
207 # Mapping from effect name to terminfo attribute name or color number.
209 # Mapping from effect name to terminfo attribute name or color number.
208 # This will also force-load the curses module.
210 # This will also force-load the curses module.
209 _terminfo_params = {'none': (True, 'sgr0'),
211 _terminfo_params = {'none': (True, 'sgr0'),
210 'standout': (True, 'smso'),
212 'standout': (True, 'smso'),
211 'underline': (True, 'smul'),
213 'underline': (True, 'smul'),
212 'reverse': (True, 'rev'),
214 'reverse': (True, 'rev'),
213 'inverse': (True, 'rev'),
215 'inverse': (True, 'rev'),
214 'blink': (True, 'blink'),
216 'blink': (True, 'blink'),
215 'dim': (True, 'dim'),
217 'dim': (True, 'dim'),
216 'bold': (True, 'bold'),
218 'bold': (True, 'bold'),
217 'invisible': (True, 'invis'),
219 'invisible': (True, 'invis'),
218 'italic': (True, 'sitm'),
220 'italic': (True, 'sitm'),
219 'black': (False, curses.COLOR_BLACK),
221 'black': (False, curses.COLOR_BLACK),
220 'red': (False, curses.COLOR_RED),
222 'red': (False, curses.COLOR_RED),
221 'green': (False, curses.COLOR_GREEN),
223 'green': (False, curses.COLOR_GREEN),
222 'yellow': (False, curses.COLOR_YELLOW),
224 'yellow': (False, curses.COLOR_YELLOW),
223 'blue': (False, curses.COLOR_BLUE),
225 'blue': (False, curses.COLOR_BLUE),
224 'magenta': (False, curses.COLOR_MAGENTA),
226 'magenta': (False, curses.COLOR_MAGENTA),
225 'cyan': (False, curses.COLOR_CYAN),
227 'cyan': (False, curses.COLOR_CYAN),
226 'white': (False, curses.COLOR_WHITE)}
228 'white': (False, curses.COLOR_WHITE)}
227 except ImportError:
229 except ImportError:
228 _terminfo_params = False
230 _terminfo_params = False
229
231
230 _styles = {'grep.match': 'red bold',
232 _styles = {'grep.match': 'red bold',
231 'grep.linenumber': 'green',
233 'grep.linenumber': 'green',
232 'grep.rev': 'green',
234 'grep.rev': 'green',
233 'grep.change': 'green',
235 'grep.change': 'green',
234 'grep.sep': 'cyan',
236 'grep.sep': 'cyan',
235 'grep.filename': 'magenta',
237 'grep.filename': 'magenta',
236 'grep.user': 'magenta',
238 'grep.user': 'magenta',
237 'grep.date': 'magenta',
239 'grep.date': 'magenta',
238 'bookmarks.current': 'green',
240 'bookmarks.current': 'green',
239 'branches.active': 'none',
241 'branches.active': 'none',
240 'branches.closed': 'black bold',
242 'branches.closed': 'black bold',
241 'branches.current': 'green',
243 'branches.current': 'green',
242 'branches.inactive': 'none',
244 'branches.inactive': 'none',
243 'diff.changed': 'white',
245 'diff.changed': 'white',
244 'diff.deleted': 'red',
246 'diff.deleted': 'red',
245 'diff.diffline': 'bold',
247 'diff.diffline': 'bold',
246 'diff.extended': 'cyan bold',
248 'diff.extended': 'cyan bold',
247 'diff.file_a': 'red bold',
249 'diff.file_a': 'red bold',
248 'diff.file_b': 'green bold',
250 'diff.file_b': 'green bold',
249 'diff.hunk': 'magenta',
251 'diff.hunk': 'magenta',
250 'diff.inserted': 'green',
252 'diff.inserted': 'green',
251 'diff.trailingwhitespace': 'bold red_background',
253 'diff.trailingwhitespace': 'bold red_background',
252 'diffstat.deleted': 'red',
254 'diffstat.deleted': 'red',
253 'diffstat.inserted': 'green',
255 'diffstat.inserted': 'green',
256 'histedit.remaining': 'red bold',
254 'ui.prompt': 'yellow',
257 'ui.prompt': 'yellow',
255 'log.changeset': 'yellow',
258 'log.changeset': 'yellow',
256 'rebase.rebased': 'blue',
259 'rebase.rebased': 'blue',
257 'rebase.remaining': 'red bold',
260 'rebase.remaining': 'red bold',
258 'resolve.resolved': 'green bold',
261 'resolve.resolved': 'green bold',
259 'resolve.unresolved': 'red bold',
262 'resolve.unresolved': 'red bold',
260 'status.added': 'green bold',
263 'status.added': 'green bold',
261 'status.clean': 'none',
264 'status.clean': 'none',
262 'status.copied': 'none',
265 'status.copied': 'none',
263 'status.deleted': 'cyan bold underline',
266 'status.deleted': 'cyan bold underline',
264 'status.ignored': 'black bold',
267 'status.ignored': 'black bold',
265 'status.modified': 'blue bold',
268 'status.modified': 'blue bold',
266 'status.removed': 'red bold',
269 'status.removed': 'red bold',
267 'status.unknown': 'magenta bold underline',
270 'status.unknown': 'magenta bold underline',
268 'tags.normal': 'green',
271 'tags.normal': 'green',
269 'tags.local': 'black bold'}
272 'tags.local': 'black bold'}
270
273
271
274
272 def _effect_str(effect):
275 def _effect_str(effect):
273 '''Helper function for render_effects().'''
276 '''Helper function for render_effects().'''
274
277
275 bg = False
278 bg = False
276 if effect.endswith('_background'):
279 if effect.endswith('_background'):
277 bg = True
280 bg = True
278 effect = effect[:-11]
281 effect = effect[:-11]
279 attr, val = _terminfo_params[effect]
282 attr, val = _terminfo_params[effect]
280 if attr:
283 if attr:
281 return curses.tigetstr(val)
284 return curses.tigetstr(val)
282 elif bg:
285 elif bg:
283 return curses.tparm(curses.tigetstr('setab'), val)
286 return curses.tparm(curses.tigetstr('setab'), val)
284 else:
287 else:
285 return curses.tparm(curses.tigetstr('setaf'), val)
288 return curses.tparm(curses.tigetstr('setaf'), val)
286
289
287 def render_effects(text, effects):
290 def render_effects(text, effects):
288 'Wrap text in commands to turn on each effect.'
291 'Wrap text in commands to turn on each effect.'
289 if not text:
292 if not text:
290 return text
293 return text
291 if not _terminfo_params:
294 if not _terminfo_params:
292 start = [str(_effects[e]) for e in ['none'] + effects.split()]
295 start = [str(_effects[e]) for e in ['none'] + effects.split()]
293 start = '\033[' + ';'.join(start) + 'm'
296 start = '\033[' + ';'.join(start) + 'm'
294 stop = '\033[' + str(_effects['none']) + 'm'
297 stop = '\033[' + str(_effects['none']) + 'm'
295 else:
298 else:
296 start = ''.join(_effect_str(effect)
299 start = ''.join(_effect_str(effect)
297 for effect in ['none'] + effects.split())
300 for effect in ['none'] + effects.split())
298 stop = _effect_str('none')
301 stop = _effect_str('none')
299 return ''.join([start, text, stop])
302 return ''.join([start, text, stop])
300
303
301 def extstyles():
304 def extstyles():
302 for name, ext in extensions.extensions():
305 for name, ext in extensions.extensions():
303 _styles.update(getattr(ext, 'colortable', {}))
306 _styles.update(getattr(ext, 'colortable', {}))
304
307
305 def configstyles(ui):
308 def configstyles(ui):
306 for status, cfgeffects in ui.configitems('color'):
309 for status, cfgeffects in ui.configitems('color'):
307 if '.' not in status or status.startswith('color.'):
310 if '.' not in status or status.startswith('color.'):
308 continue
311 continue
309 cfgeffects = ui.configlist('color', status)
312 cfgeffects = ui.configlist('color', status)
310 if cfgeffects:
313 if cfgeffects:
311 good = []
314 good = []
312 for e in cfgeffects:
315 for e in cfgeffects:
313 if not _terminfo_params and e in _effects:
316 if not _terminfo_params and e in _effects:
314 good.append(e)
317 good.append(e)
315 elif e in _terminfo_params or e[:-11] in _terminfo_params:
318 elif e in _terminfo_params or e[:-11] in _terminfo_params:
316 good.append(e)
319 good.append(e)
317 else:
320 else:
318 ui.warn(_("ignoring unknown color/effect %r "
321 ui.warn(_("ignoring unknown color/effect %r "
319 "(configured in color.%s)\n")
322 "(configured in color.%s)\n")
320 % (e, status))
323 % (e, status))
321 _styles[status] = ' '.join(good)
324 _styles[status] = ' '.join(good)
322
325
323 class colorui(uimod.ui):
326 class colorui(uimod.ui):
324 def popbuffer(self, labeled=False):
327 def popbuffer(self, labeled=False):
325 if self._colormode is None:
328 if self._colormode is None:
326 return super(colorui, self).popbuffer(labeled)
329 return super(colorui, self).popbuffer(labeled)
327
330
328 if labeled:
331 if labeled:
329 return ''.join(self.label(a, label) for a, label
332 return ''.join(self.label(a, label) for a, label
330 in self._buffers.pop())
333 in self._buffers.pop())
331 return ''.join(a for a, label in self._buffers.pop())
334 return ''.join(a for a, label in self._buffers.pop())
332
335
333 _colormode = 'ansi'
336 _colormode = 'ansi'
334 def write(self, *args, **opts):
337 def write(self, *args, **opts):
335 if self._colormode is None:
338 if self._colormode is None:
336 return super(colorui, self).write(*args, **opts)
339 return super(colorui, self).write(*args, **opts)
337
340
338 label = opts.get('label', '')
341 label = opts.get('label', '')
339 if self._buffers:
342 if self._buffers:
340 self._buffers[-1].extend([(str(a), label) for a in args])
343 self._buffers[-1].extend([(str(a), label) for a in args])
341 elif self._colormode == 'win32':
344 elif self._colormode == 'win32':
342 for a in args:
345 for a in args:
343 win32print(a, super(colorui, self).write, **opts)
346 win32print(a, super(colorui, self).write, **opts)
344 else:
347 else:
345 return super(colorui, self).write(
348 return super(colorui, self).write(
346 *[self.label(str(a), label) for a in args], **opts)
349 *[self.label(str(a), label) for a in args], **opts)
347
350
348 def write_err(self, *args, **opts):
351 def write_err(self, *args, **opts):
349 if self._colormode is None:
352 if self._colormode is None:
350 return super(colorui, self).write_err(*args, **opts)
353 return super(colorui, self).write_err(*args, **opts)
351
354
352 label = opts.get('label', '')
355 label = opts.get('label', '')
353 if self._colormode == 'win32':
356 if self._colormode == 'win32':
354 for a in args:
357 for a in args:
355 win32print(a, super(colorui, self).write_err, **opts)
358 win32print(a, super(colorui, self).write_err, **opts)
356 else:
359 else:
357 return super(colorui, self).write_err(
360 return super(colorui, self).write_err(
358 *[self.label(str(a), label) for a in args], **opts)
361 *[self.label(str(a), label) for a in args], **opts)
359
362
360 def label(self, msg, label):
363 def label(self, msg, label):
361 if self._colormode is None:
364 if self._colormode is None:
362 return super(colorui, self).label(msg, label)
365 return super(colorui, self).label(msg, label)
363
366
364 effects = []
367 effects = []
365 for l in label.split():
368 for l in label.split():
366 s = _styles.get(l, '')
369 s = _styles.get(l, '')
367 if s:
370 if s:
368 effects.append(s)
371 effects.append(s)
369 effects = ' '.join(effects)
372 effects = ' '.join(effects)
370 if effects:
373 if effects:
371 return '\n'.join([render_effects(s, effects)
374 return '\n'.join([render_effects(s, effects)
372 for s in msg.split('\n')])
375 for s in msg.split('\n')])
373 return msg
376 return msg
374
377
375 def templatelabel(context, mapping, args):
378 def templatelabel(context, mapping, args):
376 if len(args) != 2:
379 if len(args) != 2:
377 # i18n: "label" is a keyword
380 # i18n: "label" is a keyword
378 raise error.ParseError(_("label expects two arguments"))
381 raise error.ParseError(_("label expects two arguments"))
379
382
380 thing = templater.stringify(args[1][0](context, mapping, args[1][1]))
383 thing = templater.stringify(args[1][0](context, mapping, args[1][1]))
381 thing = templater.runtemplate(context, mapping,
384 thing = templater.runtemplate(context, mapping,
382 templater.compiletemplate(thing, context))
385 templater.compiletemplate(thing, context))
383
386
384 # apparently, repo could be a string that is the favicon?
387 # apparently, repo could be a string that is the favicon?
385 repo = mapping.get('repo', '')
388 repo = mapping.get('repo', '')
386 if isinstance(repo, str):
389 if isinstance(repo, str):
387 return thing
390 return thing
388
391
389 label = templater.stringify(args[0][0](context, mapping, args[0][1]))
392 label = templater.stringify(args[0][0](context, mapping, args[0][1]))
390 label = templater.runtemplate(context, mapping,
393 label = templater.runtemplate(context, mapping,
391 templater.compiletemplate(label, context))
394 templater.compiletemplate(label, context))
392
395
393 thing = templater.stringify(thing)
396 thing = templater.stringify(thing)
394 label = templater.stringify(label)
397 label = templater.stringify(label)
395
398
396 return repo.ui.label(thing, label)
399 return repo.ui.label(thing, label)
397
400
398 def uisetup(ui):
401 def uisetup(ui):
399 if ui.plain():
402 if ui.plain():
400 return
403 return
401 if not issubclass(ui.__class__, colorui):
404 if not issubclass(ui.__class__, colorui):
402 colorui.__bases__ = (ui.__class__,)
405 colorui.__bases__ = (ui.__class__,)
403 ui.__class__ = colorui
406 ui.__class__ = colorui
404 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
407 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
405 mode = _modesetup(ui_, opts)
408 mode = _modesetup(ui_, opts)
406 colorui._colormode = mode
409 colorui._colormode = mode
407 if mode:
410 if mode:
408 extstyles()
411 extstyles()
409 configstyles(ui_)
412 configstyles(ui_)
410 return orig(ui_, opts, cmd, cmdfunc)
413 return orig(ui_, opts, cmd, cmdfunc)
411 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
414 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
412 templater.funcs['label'] = templatelabel
415 templater.funcs['label'] = templatelabel
413
416
414 def extsetup(ui):
417 def extsetup(ui):
415 commands.globalopts.append(
418 commands.globalopts.append(
416 ('', 'color', 'auto',
419 ('', 'color', 'auto',
417 # i18n: 'always', 'auto', and 'never' are keywords and should
420 # i18n: 'always', 'auto', and 'never' are keywords and should
418 # not be translated
421 # not be translated
419 _("when to colorize (boolean, always, auto, or never)"),
422 _("when to colorize (boolean, always, auto, or never)"),
420 _('TYPE')))
423 _('TYPE')))
421
424
422 if os.name != 'nt':
425 if os.name != 'nt':
423 w32effects = None
426 w32effects = None
424 else:
427 else:
425 import re, ctypes
428 import re, ctypes
426
429
427 _kernel32 = ctypes.windll.kernel32
430 _kernel32 = ctypes.windll.kernel32
428
431
429 _WORD = ctypes.c_ushort
432 _WORD = ctypes.c_ushort
430
433
431 _INVALID_HANDLE_VALUE = -1
434 _INVALID_HANDLE_VALUE = -1
432
435
433 class _COORD(ctypes.Structure):
436 class _COORD(ctypes.Structure):
434 _fields_ = [('X', ctypes.c_short),
437 _fields_ = [('X', ctypes.c_short),
435 ('Y', ctypes.c_short)]
438 ('Y', ctypes.c_short)]
436
439
437 class _SMALL_RECT(ctypes.Structure):
440 class _SMALL_RECT(ctypes.Structure):
438 _fields_ = [('Left', ctypes.c_short),
441 _fields_ = [('Left', ctypes.c_short),
439 ('Top', ctypes.c_short),
442 ('Top', ctypes.c_short),
440 ('Right', ctypes.c_short),
443 ('Right', ctypes.c_short),
441 ('Bottom', ctypes.c_short)]
444 ('Bottom', ctypes.c_short)]
442
445
443 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
446 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
444 _fields_ = [('dwSize', _COORD),
447 _fields_ = [('dwSize', _COORD),
445 ('dwCursorPosition', _COORD),
448 ('dwCursorPosition', _COORD),
446 ('wAttributes', _WORD),
449 ('wAttributes', _WORD),
447 ('srWindow', _SMALL_RECT),
450 ('srWindow', _SMALL_RECT),
448 ('dwMaximumWindowSize', _COORD)]
451 ('dwMaximumWindowSize', _COORD)]
449
452
450 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
453 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
451 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
454 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
452
455
453 _FOREGROUND_BLUE = 0x0001
456 _FOREGROUND_BLUE = 0x0001
454 _FOREGROUND_GREEN = 0x0002
457 _FOREGROUND_GREEN = 0x0002
455 _FOREGROUND_RED = 0x0004
458 _FOREGROUND_RED = 0x0004
456 _FOREGROUND_INTENSITY = 0x0008
459 _FOREGROUND_INTENSITY = 0x0008
457
460
458 _BACKGROUND_BLUE = 0x0010
461 _BACKGROUND_BLUE = 0x0010
459 _BACKGROUND_GREEN = 0x0020
462 _BACKGROUND_GREEN = 0x0020
460 _BACKGROUND_RED = 0x0040
463 _BACKGROUND_RED = 0x0040
461 _BACKGROUND_INTENSITY = 0x0080
464 _BACKGROUND_INTENSITY = 0x0080
462
465
463 _COMMON_LVB_REVERSE_VIDEO = 0x4000
466 _COMMON_LVB_REVERSE_VIDEO = 0x4000
464 _COMMON_LVB_UNDERSCORE = 0x8000
467 _COMMON_LVB_UNDERSCORE = 0x8000
465
468
466 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
469 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
467 w32effects = {
470 w32effects = {
468 'none': -1,
471 'none': -1,
469 'black': 0,
472 'black': 0,
470 'red': _FOREGROUND_RED,
473 'red': _FOREGROUND_RED,
471 'green': _FOREGROUND_GREEN,
474 'green': _FOREGROUND_GREEN,
472 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
475 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
473 'blue': _FOREGROUND_BLUE,
476 'blue': _FOREGROUND_BLUE,
474 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
477 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
475 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
478 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
476 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
479 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
477 'bold': _FOREGROUND_INTENSITY,
480 'bold': _FOREGROUND_INTENSITY,
478 'black_background': 0x100, # unused value > 0x0f
481 'black_background': 0x100, # unused value > 0x0f
479 'red_background': _BACKGROUND_RED,
482 'red_background': _BACKGROUND_RED,
480 'green_background': _BACKGROUND_GREEN,
483 'green_background': _BACKGROUND_GREEN,
481 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
484 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
482 'blue_background': _BACKGROUND_BLUE,
485 'blue_background': _BACKGROUND_BLUE,
483 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
486 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
484 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
487 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
485 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
488 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
486 _BACKGROUND_BLUE),
489 _BACKGROUND_BLUE),
487 'bold_background': _BACKGROUND_INTENSITY,
490 'bold_background': _BACKGROUND_INTENSITY,
488 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
491 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
489 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
492 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
490 }
493 }
491
494
492 passthrough = set([_FOREGROUND_INTENSITY,
495 passthrough = set([_FOREGROUND_INTENSITY,
493 _BACKGROUND_INTENSITY,
496 _BACKGROUND_INTENSITY,
494 _COMMON_LVB_UNDERSCORE,
497 _COMMON_LVB_UNDERSCORE,
495 _COMMON_LVB_REVERSE_VIDEO])
498 _COMMON_LVB_REVERSE_VIDEO])
496
499
497 stdout = _kernel32.GetStdHandle(
500 stdout = _kernel32.GetStdHandle(
498 _STD_OUTPUT_HANDLE) # don't close the handle returned
501 _STD_OUTPUT_HANDLE) # don't close the handle returned
499 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
502 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
500 w32effects = None
503 w32effects = None
501 else:
504 else:
502 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
505 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
503 if not _kernel32.GetConsoleScreenBufferInfo(
506 if not _kernel32.GetConsoleScreenBufferInfo(
504 stdout, ctypes.byref(csbi)):
507 stdout, ctypes.byref(csbi)):
505 # stdout may not support GetConsoleScreenBufferInfo()
508 # stdout may not support GetConsoleScreenBufferInfo()
506 # when called from subprocess or redirected
509 # when called from subprocess or redirected
507 w32effects = None
510 w32effects = None
508 else:
511 else:
509 origattr = csbi.wAttributes
512 origattr = csbi.wAttributes
510 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
513 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
511 re.MULTILINE | re.DOTALL)
514 re.MULTILINE | re.DOTALL)
512
515
513 def win32print(text, orig, **opts):
516 def win32print(text, orig, **opts):
514 label = opts.get('label', '')
517 label = opts.get('label', '')
515 attr = origattr
518 attr = origattr
516
519
517 def mapcolor(val, attr):
520 def mapcolor(val, attr):
518 if val == -1:
521 if val == -1:
519 return origattr
522 return origattr
520 elif val in passthrough:
523 elif val in passthrough:
521 return attr | val
524 return attr | val
522 elif val > 0x0f:
525 elif val > 0x0f:
523 return (val & 0x70) | (attr & 0x8f)
526 return (val & 0x70) | (attr & 0x8f)
524 else:
527 else:
525 return (val & 0x07) | (attr & 0xf8)
528 return (val & 0x07) | (attr & 0xf8)
526
529
527 # determine console attributes based on labels
530 # determine console attributes based on labels
528 for l in label.split():
531 for l in label.split():
529 style = _styles.get(l, '')
532 style = _styles.get(l, '')
530 for effect in style.split():
533 for effect in style.split():
531 attr = mapcolor(w32effects[effect], attr)
534 attr = mapcolor(w32effects[effect], attr)
532
535
533 # hack to ensure regexp finds data
536 # hack to ensure regexp finds data
534 if not text.startswith('\033['):
537 if not text.startswith('\033['):
535 text = '\033[m' + text
538 text = '\033[m' + text
536
539
537 # Look for ANSI-like codes embedded in text
540 # Look for ANSI-like codes embedded in text
538 m = re.match(ansire, text)
541 m = re.match(ansire, text)
539
542
540 try:
543 try:
541 while m:
544 while m:
542 for sattr in m.group(1).split(';'):
545 for sattr in m.group(1).split(';'):
543 if sattr:
546 if sattr:
544 attr = mapcolor(int(sattr), attr)
547 attr = mapcolor(int(sattr), attr)
545 _kernel32.SetConsoleTextAttribute(stdout, attr)
548 _kernel32.SetConsoleTextAttribute(stdout, attr)
546 orig(m.group(2), **opts)
549 orig(m.group(2), **opts)
547 m = re.match(ansire, m.group(3))
550 m = re.match(ansire, m.group(3))
548 finally:
551 finally:
549 # Explicitly reset original attributes
552 # Explicitly reset original attributes
550 _kernel32.SetConsoleTextAttribute(stdout, origattr)
553 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,858 +1,871 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
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 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commands:
33 # Commands:
34 # p, pick = use commit
34 # p, pick = use commit
35 # e, edit = use commit, but stop for amending
35 # e, edit = use commit, but stop for amending
36 # f, fold = use commit, but fold into previous commit (combines N and N-1)
36 # f, fold = use commit, but fold into previous commit (combines N and N-1)
37 # d, drop = remove commit from history
37 # d, drop = remove commit from history
38 # m, mess = edit message without changing commit content
38 # m, mess = edit message without changing commit content
39 #
39 #
40
40
41 In this file, lines beginning with ``#`` are ignored. You must specify a rule
41 In this file, lines beginning with ``#`` are ignored. You must specify a rule
42 for each revision in your history. For example, if you had meant to add gamma
42 for each revision in your history. For example, if you had meant to add gamma
43 before beta, and then wanted to add delta in the same revision as beta, you
43 before beta, and then wanted to add delta in the same revision as beta, you
44 would reorganize the file to look like this::
44 would reorganize the file to look like this::
45
45
46 pick 030b686bedc4 Add gamma
46 pick 030b686bedc4 Add gamma
47 pick c561b4e977df Add beta
47 pick c561b4e977df Add beta
48 fold 7c2fd3b9020c Add delta
48 fold 7c2fd3b9020c Add delta
49
49
50 # Edit history between c561b4e977df and 7c2fd3b9020c
50 # Edit history between c561b4e977df and 7c2fd3b9020c
51 #
51 #
52 # Commands:
52 # Commands:
53 # p, pick = use commit
53 # p, pick = use commit
54 # e, edit = use commit, but stop for amending
54 # e, edit = use commit, but stop for amending
55 # f, fold = use commit, but fold into previous commit (combines N and N-1)
55 # f, fold = use commit, but fold into previous commit (combines N and N-1)
56 # d, drop = remove commit from history
56 # d, drop = remove commit from history
57 # m, mess = edit message without changing commit content
57 # m, mess = edit message without changing commit content
58 #
58 #
59
59
60 At which point you close the editor and ``histedit`` starts working. When you
60 At which point you close the editor and ``histedit`` starts working. When you
61 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
61 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
62 those revisions together, offering you a chance to clean up the commit message::
62 those revisions together, offering you a chance to clean up the commit message::
63
63
64 Add beta
64 Add beta
65 ***
65 ***
66 Add delta
66 Add delta
67
67
68 Edit the commit message to your liking, then close the editor. For
68 Edit the commit message to your liking, then close the editor. For
69 this example, let's assume that the commit message was changed to
69 this example, let's assume that the commit message was changed to
70 ``Add beta and delta.`` After histedit has run and had a chance to
70 ``Add beta and delta.`` After histedit has run and had a chance to
71 remove any old or temporary revisions it needed, the history looks
71 remove any old or temporary revisions it needed, the history looks
72 like this::
72 like this::
73
73
74 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
74 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
75 | Add beta and delta.
75 | Add beta and delta.
76 |
76 |
77 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
77 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
78 | Add gamma
78 | Add gamma
79 |
79 |
80 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
80 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
81 Add alpha
81 Add alpha
82
82
83 Note that ``histedit`` does *not* remove any revisions (even its own temporary
83 Note that ``histedit`` does *not* remove any revisions (even its own temporary
84 ones) until after it has completed all the editing operations, so it will
84 ones) until after it has completed all the editing operations, so it will
85 probably perform several strip operations when it's done. For the above example,
85 probably perform several strip operations when it's done. For the above example,
86 it had to run strip twice. Strip can be slow depending on a variety of factors,
86 it had to run strip twice. Strip can be slow depending on a variety of factors,
87 so you might need to be a little patient. You can choose to keep the original
87 so you might need to be a little patient. You can choose to keep the original
88 revisions by passing the ``--keep`` flag.
88 revisions by passing the ``--keep`` flag.
89
89
90 The ``edit`` operation will drop you back to a command prompt,
90 The ``edit`` operation will drop you back to a command prompt,
91 allowing you to edit files freely, or even use ``hg record`` to commit
91 allowing you to edit files freely, or even use ``hg record`` to commit
92 some changes as a separate commit. When you're done, any remaining
92 some changes as a separate commit. When you're done, any remaining
93 uncommitted changes will be committed as well. When done, run ``hg
93 uncommitted changes will be committed as well. When done, run ``hg
94 histedit --continue`` to finish this step. You'll be prompted for a
94 histedit --continue`` to finish this step. You'll be prompted for a
95 new commit message, but the default commit message will be the
95 new commit message, but the default commit message will be the
96 original message for the ``edit`` ed revision.
96 original message for the ``edit`` ed revision.
97
97
98 The ``message`` operation will give you a chance to revise a commit
98 The ``message`` operation will give you a chance to revise a commit
99 message without changing the contents. It's a shortcut for doing
99 message without changing the contents. It's a shortcut for doing
100 ``edit`` immediately followed by `hg histedit --continue``.
100 ``edit`` immediately followed by `hg histedit --continue``.
101
101
102 If ``histedit`` encounters a conflict when moving a revision (while
102 If ``histedit`` encounters a conflict when moving a revision (while
103 handling ``pick`` or ``fold``), it'll stop in a similar manner to
103 handling ``pick`` or ``fold``), it'll stop in a similar manner to
104 ``edit`` with the difference that it won't prompt you for a commit
104 ``edit`` with the difference that it won't prompt you for a commit
105 message when done. If you decide at this point that you don't like how
105 message when done. If you decide at this point that you don't like how
106 much work it will be to rearrange history, or that you made a mistake,
106 much work it will be to rearrange history, or that you made a mistake,
107 you can use ``hg histedit --abort`` to abandon the new changes you
107 you can use ``hg histedit --abort`` to abandon the new changes you
108 have made and return to the state before you attempted to edit your
108 have made and return to the state before you attempted to edit your
109 history.
109 history.
110
110
111 If we clone the histedit-ed example repository above and add four more
111 If we clone the histedit-ed example repository above and add four more
112 changes, such that we have the following history::
112 changes, such that we have the following history::
113
113
114 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
114 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
115 | Add theta
115 | Add theta
116 |
116 |
117 o 5 140988835471 2009-04-27 18:04 -0500 stefan
117 o 5 140988835471 2009-04-27 18:04 -0500 stefan
118 | Add eta
118 | Add eta
119 |
119 |
120 o 4 122930637314 2009-04-27 18:04 -0500 stefan
120 o 4 122930637314 2009-04-27 18:04 -0500 stefan
121 | Add zeta
121 | Add zeta
122 |
122 |
123 o 3 836302820282 2009-04-27 18:04 -0500 stefan
123 o 3 836302820282 2009-04-27 18:04 -0500 stefan
124 | Add epsilon
124 | Add epsilon
125 |
125 |
126 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
126 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
127 | Add beta and delta.
127 | Add beta and delta.
128 |
128 |
129 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
129 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
130 | Add gamma
130 | Add gamma
131 |
131 |
132 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
132 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
133 Add alpha
133 Add alpha
134
134
135 If you run ``hg histedit --outgoing`` on the clone then it is the same
135 If you run ``hg histedit --outgoing`` on the clone then it is the same
136 as running ``hg histedit 836302820282``. If you need plan to push to a
136 as running ``hg histedit 836302820282``. If you need plan to push to a
137 repository that Mercurial does not detect to be related to the source
137 repository that Mercurial does not detect to be related to the source
138 repo, you can add a ``--force`` option.
138 repo, you can add a ``--force`` option.
139 """
139 """
140
140
141 try:
141 try:
142 import cPickle as pickle
142 import cPickle as pickle
143 except ImportError:
143 except ImportError:
144 import pickle
144 import pickle
145 import os
145 import os
146 import sys
146 import sys
147
147
148 from mercurial import cmdutil
148 from mercurial import cmdutil
149 from mercurial import discovery
149 from mercurial import discovery
150 from mercurial import error
150 from mercurial import error
151 from mercurial import copies
151 from mercurial import copies
152 from mercurial import context
152 from mercurial import context
153 from mercurial import hg
153 from mercurial import hg
154 from mercurial import lock as lockmod
154 from mercurial import lock as lockmod
155 from mercurial import node
155 from mercurial import node
156 from mercurial import repair
156 from mercurial import repair
157 from mercurial import scmutil
157 from mercurial import scmutil
158 from mercurial import util
158 from mercurial import util
159 from mercurial import obsolete
159 from mercurial import obsolete
160 from mercurial import merge as mergemod
160 from mercurial import merge as mergemod
161 from mercurial.i18n import _
161 from mercurial.i18n import _
162
162
163 cmdtable = {}
163 cmdtable = {}
164 command = cmdutil.command(cmdtable)
164 command = cmdutil.command(cmdtable)
165
165
166 testedwith = 'internal'
166 testedwith = 'internal'
167
167
168 # i18n: command names and abbreviations must remain untranslated
168 # i18n: command names and abbreviations must remain untranslated
169 editcomment = _("""# Edit history between %s and %s
169 editcomment = _("""# Edit history between %s and %s
170 #
170 #
171 # Commands:
171 # Commands:
172 # p, pick = use commit
172 # p, pick = use commit
173 # e, edit = use commit, but stop for amending
173 # e, edit = use commit, but stop for amending
174 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 # f, fold = use commit, but fold into previous commit (combines N and N-1)
175 # d, drop = remove commit from history
175 # d, drop = remove commit from history
176 # m, mess = edit message without changing commit content
176 # m, mess = edit message without changing commit content
177 #
177 #
178 """)
178 """)
179
179
180 def commitfuncfor(repo, src):
180 def commitfuncfor(repo, src):
181 """Build a commit function for the replacement of <src>
181 """Build a commit function for the replacement of <src>
182
182
183 This function ensure we apply the same treatment to all changesets.
183 This function ensure we apply the same treatment to all changesets.
184
184
185 - Add a 'histedit_source' entry in extra.
185 - Add a 'histedit_source' entry in extra.
186
186
187 Note that fold have its own separated logic because its handling is a bit
187 Note that fold have its own separated logic because its handling is a bit
188 different and not easily factored out of the fold method.
188 different and not easily factored out of the fold method.
189 """
189 """
190 phasemin = src.phase()
190 phasemin = src.phase()
191 def commitfunc(**kwargs):
191 def commitfunc(**kwargs):
192 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
192 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
193 try:
193 try:
194 repo.ui.setconfig('phases', 'new-commit', phasemin)
194 repo.ui.setconfig('phases', 'new-commit', phasemin)
195 extra = kwargs.get('extra', {}).copy()
195 extra = kwargs.get('extra', {}).copy()
196 extra['histedit_source'] = src.hex()
196 extra['histedit_source'] = src.hex()
197 kwargs['extra'] = extra
197 kwargs['extra'] = extra
198 return repo.commit(**kwargs)
198 return repo.commit(**kwargs)
199 finally:
199 finally:
200 repo.ui.restoreconfig(phasebackup)
200 repo.ui.restoreconfig(phasebackup)
201 return commitfunc
201 return commitfunc
202
202
203
203
204
204
205 def applychanges(ui, repo, ctx, opts):
205 def applychanges(ui, repo, ctx, opts):
206 """Merge changeset from ctx (only) in the current working directory"""
206 """Merge changeset from ctx (only) in the current working directory"""
207 wcpar = repo.dirstate.parents()[0]
207 wcpar = repo.dirstate.parents()[0]
208 if ctx.p1().node() == wcpar:
208 if ctx.p1().node() == wcpar:
209 # edition ar "in place" we do not need to make any merge,
209 # edition ar "in place" we do not need to make any merge,
210 # just applies changes on parent for edition
210 # just applies changes on parent for edition
211 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
211 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
212 stats = None
212 stats = None
213 else:
213 else:
214 try:
214 try:
215 # ui.forcemerge is an internal variable, do not document
215 # ui.forcemerge is an internal variable, do not document
216 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
216 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
217 stats = mergemod.update(repo, ctx.node(), True, True, False,
217 stats = mergemod.update(repo, ctx.node(), True, True, False,
218 ctx.p1().node())
218 ctx.p1().node())
219 finally:
219 finally:
220 repo.ui.setconfig('ui', 'forcemerge', '')
220 repo.ui.setconfig('ui', 'forcemerge', '')
221 repo.setparents(wcpar, node.nullid)
221 repo.setparents(wcpar, node.nullid)
222 repo.dirstate.write()
222 repo.dirstate.write()
223 # fix up dirstate for copies and renames
223 # fix up dirstate for copies and renames
224 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
224 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
225 return stats
225 return stats
226
226
227 def collapse(repo, first, last, commitopts):
227 def collapse(repo, first, last, commitopts):
228 """collapse the set of revisions from first to last as new one.
228 """collapse the set of revisions from first to last as new one.
229
229
230 Expected commit options are:
230 Expected commit options are:
231 - message
231 - message
232 - date
232 - date
233 - username
233 - username
234 Commit message is edited in all cases.
234 Commit message is edited in all cases.
235
235
236 This function works in memory."""
236 This function works in memory."""
237 ctxs = list(repo.set('%d::%d', first, last))
237 ctxs = list(repo.set('%d::%d', first, last))
238 if not ctxs:
238 if not ctxs:
239 return None
239 return None
240 base = first.parents()[0]
240 base = first.parents()[0]
241
241
242 # commit a new version of the old changeset, including the update
242 # commit a new version of the old changeset, including the update
243 # collect all files which might be affected
243 # collect all files which might be affected
244 files = set()
244 files = set()
245 for ctx in ctxs:
245 for ctx in ctxs:
246 files.update(ctx.files())
246 files.update(ctx.files())
247
247
248 # Recompute copies (avoid recording a -> b -> a)
248 # Recompute copies (avoid recording a -> b -> a)
249 copied = copies.pathcopies(first, last)
249 copied = copies.pathcopies(first, last)
250
250
251 # prune files which were reverted by the updates
251 # prune files which were reverted by the updates
252 def samefile(f):
252 def samefile(f):
253 if f in last.manifest():
253 if f in last.manifest():
254 a = last.filectx(f)
254 a = last.filectx(f)
255 if f in base.manifest():
255 if f in base.manifest():
256 b = base.filectx(f)
256 b = base.filectx(f)
257 return (a.data() == b.data()
257 return (a.data() == b.data()
258 and a.flags() == b.flags())
258 and a.flags() == b.flags())
259 else:
259 else:
260 return False
260 return False
261 else:
261 else:
262 return f not in base.manifest()
262 return f not in base.manifest()
263 files = [f for f in files if not samefile(f)]
263 files = [f for f in files if not samefile(f)]
264 # commit version of these files as defined by head
264 # commit version of these files as defined by head
265 headmf = last.manifest()
265 headmf = last.manifest()
266 def filectxfn(repo, ctx, path):
266 def filectxfn(repo, ctx, path):
267 if path in headmf:
267 if path in headmf:
268 fctx = last[path]
268 fctx = last[path]
269 flags = fctx.flags()
269 flags = fctx.flags()
270 mctx = context.memfilectx(fctx.path(), fctx.data(),
270 mctx = context.memfilectx(fctx.path(), fctx.data(),
271 islink='l' in flags,
271 islink='l' in flags,
272 isexec='x' in flags,
272 isexec='x' in flags,
273 copied=copied.get(path))
273 copied=copied.get(path))
274 return mctx
274 return mctx
275 raise IOError()
275 raise IOError()
276
276
277 if commitopts.get('message'):
277 if commitopts.get('message'):
278 message = commitopts['message']
278 message = commitopts['message']
279 else:
279 else:
280 message = first.description()
280 message = first.description()
281 user = commitopts.get('user')
281 user = commitopts.get('user')
282 date = commitopts.get('date')
282 date = commitopts.get('date')
283 extra = commitopts.get('extra')
283 extra = commitopts.get('extra')
284
284
285 parents = (first.p1().node(), first.p2().node())
285 parents = (first.p1().node(), first.p2().node())
286 new = context.memctx(repo,
286 new = context.memctx(repo,
287 parents=parents,
287 parents=parents,
288 text=message,
288 text=message,
289 files=files,
289 files=files,
290 filectxfn=filectxfn,
290 filectxfn=filectxfn,
291 user=user,
291 user=user,
292 date=date,
292 date=date,
293 extra=extra)
293 extra=extra)
294 new._text = cmdutil.commitforceeditor(repo, new, [])
294 new._text = cmdutil.commitforceeditor(repo, new, [])
295 return repo.commitctx(new)
295 return repo.commitctx(new)
296
296
297 def pick(ui, repo, ctx, ha, opts):
297 def pick(ui, repo, ctx, ha, opts):
298 oldctx = repo[ha]
298 oldctx = repo[ha]
299 if oldctx.parents()[0] == ctx:
299 if oldctx.parents()[0] == ctx:
300 ui.debug('node %s unchanged\n' % ha)
300 ui.debug('node %s unchanged\n' % ha)
301 return oldctx, []
301 return oldctx, []
302 hg.update(repo, ctx.node())
302 hg.update(repo, ctx.node())
303 stats = applychanges(ui, repo, oldctx, opts)
303 stats = applychanges(ui, repo, oldctx, opts)
304 if stats and stats[3] > 0:
304 if stats and stats[3] > 0:
305 raise error.InterventionRequired(_('Fix up the change and run '
305 raise error.InterventionRequired(_('Fix up the change and run '
306 'hg histedit --continue'))
306 'hg histedit --continue'))
307 # drop the second merge parent
307 # drop the second merge parent
308 commit = commitfuncfor(repo, oldctx)
308 commit = commitfuncfor(repo, oldctx)
309 n = commit(text=oldctx.description(), user=oldctx.user(),
309 n = commit(text=oldctx.description(), user=oldctx.user(),
310 date=oldctx.date(), extra=oldctx.extra())
310 date=oldctx.date(), extra=oldctx.extra())
311 if n is None:
311 if n is None:
312 ui.warn(_('%s: empty changeset\n')
312 ui.warn(_('%s: empty changeset\n')
313 % node.hex(ha))
313 % node.hex(ha))
314 return ctx, []
314 return ctx, []
315 new = repo[n]
315 new = repo[n]
316 return new, [(oldctx.node(), (n,))]
316 return new, [(oldctx.node(), (n,))]
317
317
318
318
319 def edit(ui, repo, ctx, ha, opts):
319 def edit(ui, repo, ctx, ha, opts):
320 oldctx = repo[ha]
320 oldctx = repo[ha]
321 hg.update(repo, ctx.node())
321 hg.update(repo, ctx.node())
322 applychanges(ui, repo, oldctx, opts)
322 applychanges(ui, repo, oldctx, opts)
323 raise error.InterventionRequired(
323 raise error.InterventionRequired(
324 _('Make changes as needed, you may commit or record as needed now.\n'
324 _('Make changes as needed, you may commit or record as needed now.\n'
325 'When you are finished, run hg histedit --continue to resume.'))
325 'When you are finished, run hg histedit --continue to resume.'))
326
326
327 def fold(ui, repo, ctx, ha, opts):
327 def fold(ui, repo, ctx, ha, opts):
328 oldctx = repo[ha]
328 oldctx = repo[ha]
329 hg.update(repo, ctx.node())
329 hg.update(repo, ctx.node())
330 stats = applychanges(ui, repo, oldctx, opts)
330 stats = applychanges(ui, repo, oldctx, opts)
331 if stats and stats[3] > 0:
331 if stats and stats[3] > 0:
332 raise error.InterventionRequired(
332 raise error.InterventionRequired(
333 _('Fix up the change and run hg histedit --continue'))
333 _('Fix up the change and run hg histedit --continue'))
334 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
334 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
335 date=oldctx.date(), extra=oldctx.extra())
335 date=oldctx.date(), extra=oldctx.extra())
336 if n is None:
336 if n is None:
337 ui.warn(_('%s: empty changeset')
337 ui.warn(_('%s: empty changeset')
338 % node.hex(ha))
338 % node.hex(ha))
339 return ctx, []
339 return ctx, []
340 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
340 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
341
341
342 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
342 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
343 parent = ctx.parents()[0].node()
343 parent = ctx.parents()[0].node()
344 hg.update(repo, parent)
344 hg.update(repo, parent)
345 ### prepare new commit data
345 ### prepare new commit data
346 commitopts = opts.copy()
346 commitopts = opts.copy()
347 # username
347 # username
348 if ctx.user() == oldctx.user():
348 if ctx.user() == oldctx.user():
349 username = ctx.user()
349 username = ctx.user()
350 else:
350 else:
351 username = ui.username()
351 username = ui.username()
352 commitopts['user'] = username
352 commitopts['user'] = username
353 # commit message
353 # commit message
354 newmessage = '\n***\n'.join(
354 newmessage = '\n***\n'.join(
355 [ctx.description()] +
355 [ctx.description()] +
356 [repo[r].description() for r in internalchanges] +
356 [repo[r].description() for r in internalchanges] +
357 [oldctx.description()]) + '\n'
357 [oldctx.description()]) + '\n'
358 commitopts['message'] = newmessage
358 commitopts['message'] = newmessage
359 # date
359 # date
360 commitopts['date'] = max(ctx.date(), oldctx.date())
360 commitopts['date'] = max(ctx.date(), oldctx.date())
361 extra = ctx.extra().copy()
361 extra = ctx.extra().copy()
362 # histedit_source
362 # histedit_source
363 # note: ctx is likely a temporary commit but that the best we can do here
363 # note: ctx is likely a temporary commit but that the best we can do here
364 # This is sufficient to solve issue3681 anyway
364 # This is sufficient to solve issue3681 anyway
365 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
365 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
366 commitopts['extra'] = extra
366 commitopts['extra'] = extra
367 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
367 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
368 try:
368 try:
369 phasemin = max(ctx.phase(), oldctx.phase())
369 phasemin = max(ctx.phase(), oldctx.phase())
370 repo.ui.setconfig('phases', 'new-commit', phasemin)
370 repo.ui.setconfig('phases', 'new-commit', phasemin)
371 n = collapse(repo, ctx, repo[newnode], commitopts)
371 n = collapse(repo, ctx, repo[newnode], commitopts)
372 finally:
372 finally:
373 repo.ui.restoreconfig(phasebackup)
373 repo.ui.restoreconfig(phasebackup)
374 if n is None:
374 if n is None:
375 return ctx, []
375 return ctx, []
376 hg.update(repo, n)
376 hg.update(repo, n)
377 replacements = [(oldctx.node(), (newnode,)),
377 replacements = [(oldctx.node(), (newnode,)),
378 (ctx.node(), (n,)),
378 (ctx.node(), (n,)),
379 (newnode, (n,)),
379 (newnode, (n,)),
380 ]
380 ]
381 for ich in internalchanges:
381 for ich in internalchanges:
382 replacements.append((ich, (n,)))
382 replacements.append((ich, (n,)))
383 return repo[n], replacements
383 return repo[n], replacements
384
384
385 def drop(ui, repo, ctx, ha, opts):
385 def drop(ui, repo, ctx, ha, opts):
386 return ctx, [(repo[ha].node(), ())]
386 return ctx, [(repo[ha].node(), ())]
387
387
388
388
389 def message(ui, repo, ctx, ha, opts):
389 def message(ui, repo, ctx, ha, opts):
390 oldctx = repo[ha]
390 oldctx = repo[ha]
391 hg.update(repo, ctx.node())
391 hg.update(repo, ctx.node())
392 stats = applychanges(ui, repo, oldctx, opts)
392 stats = applychanges(ui, repo, oldctx, opts)
393 if stats and stats[3] > 0:
393 if stats and stats[3] > 0:
394 raise error.InterventionRequired(
394 raise error.InterventionRequired(
395 _('Fix up the change and run hg histedit --continue'))
395 _('Fix up the change and run hg histedit --continue'))
396 message = oldctx.description() + '\n'
396 message = oldctx.description() + '\n'
397 message = ui.edit(message, ui.username())
397 message = ui.edit(message, ui.username())
398 commit = commitfuncfor(repo, oldctx)
398 commit = commitfuncfor(repo, oldctx)
399 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
399 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
400 extra=oldctx.extra())
400 extra=oldctx.extra())
401 newctx = repo[new]
401 newctx = repo[new]
402 if oldctx.node() != newctx.node():
402 if oldctx.node() != newctx.node():
403 return newctx, [(oldctx.node(), (new,))]
403 return newctx, [(oldctx.node(), (new,))]
404 # We didn't make an edit, so just indicate no replaced nodes
404 # We didn't make an edit, so just indicate no replaced nodes
405 return newctx, []
405 return newctx, []
406
406
407 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
407 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
408 """utility function to find the first outgoing changeset
408 """utility function to find the first outgoing changeset
409
409
410 Used by initialisation code"""
410 Used by initialisation code"""
411 dest = ui.expandpath(remote or 'default-push', remote or 'default')
411 dest = ui.expandpath(remote or 'default-push', remote or 'default')
412 dest, revs = hg.parseurl(dest, None)[:2]
412 dest, revs = hg.parseurl(dest, None)[:2]
413 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
413 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
414
414
415 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
415 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
416 other = hg.peer(repo, opts, dest)
416 other = hg.peer(repo, opts, dest)
417
417
418 if revs:
418 if revs:
419 revs = [repo.lookup(rev) for rev in revs]
419 revs = [repo.lookup(rev) for rev in revs]
420
420
421 # hexlify nodes from outgoing, because we're going to parse
421 # hexlify nodes from outgoing, because we're going to parse
422 # parent[0] using revsingle below, and if the binary hash
422 # parent[0] using revsingle below, and if the binary hash
423 # contains special revset characters like ":" the revset
423 # contains special revset characters like ":" the revset
424 # parser can choke.
424 # parser can choke.
425 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
425 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
426 if not outgoing.missing:
426 if not outgoing.missing:
427 raise util.Abort(_('no outgoing ancestors'))
427 raise util.Abort(_('no outgoing ancestors'))
428 return outgoing.missing[0]
428 return outgoing.missing[0]
429
429
430 actiontable = {'p': pick,
430 actiontable = {'p': pick,
431 'pick': pick,
431 'pick': pick,
432 'e': edit,
432 'e': edit,
433 'edit': edit,
433 'edit': edit,
434 'f': fold,
434 'f': fold,
435 'fold': fold,
435 'fold': fold,
436 'd': drop,
436 'd': drop,
437 'drop': drop,
437 'drop': drop,
438 'm': message,
438 'm': message,
439 'mess': message,
439 'mess': message,
440 }
440 }
441
441
442 @command('histedit',
442 @command('histedit',
443 [('', 'commands', '',
443 [('', 'commands', '',
444 _('Read history edits from the specified file.')),
444 _('Read history edits from the specified file.')),
445 ('c', 'continue', False, _('continue an edit already in progress')),
445 ('c', 'continue', False, _('continue an edit already in progress')),
446 ('k', 'keep', False,
446 ('k', 'keep', False,
447 _("don't strip old nodes after edit is complete")),
447 _("don't strip old nodes after edit is complete")),
448 ('', 'abort', False, _('abort an edit in progress')),
448 ('', 'abort', False, _('abort an edit in progress')),
449 ('o', 'outgoing', False, _('changesets not found in destination')),
449 ('o', 'outgoing', False, _('changesets not found in destination')),
450 ('f', 'force', False,
450 ('f', 'force', False,
451 _('force outgoing even for unrelated repositories')),
451 _('force outgoing even for unrelated repositories')),
452 ('r', 'rev', [], _('first revision to be edited'))],
452 ('r', 'rev', [], _('first revision to be edited'))],
453 _("[PARENT]"))
453 _("[PARENT]"))
454 def histedit(ui, repo, *freeargs, **opts):
454 def histedit(ui, repo, *freeargs, **opts):
455 """interactively edit changeset history
455 """interactively edit changeset history
456 """
456 """
457 # TODO only abort if we try and histedit mq patches, not just
457 # TODO only abort if we try and histedit mq patches, not just
458 # blanket if mq patches are applied somewhere
458 # blanket if mq patches are applied somewhere
459 mq = getattr(repo, 'mq', None)
459 mq = getattr(repo, 'mq', None)
460 if mq and mq.applied:
460 if mq and mq.applied:
461 raise util.Abort(_('source has mq patches applied'))
461 raise util.Abort(_('source has mq patches applied'))
462
462
463 # basic argument incompatibility processing
463 # basic argument incompatibility processing
464 outg = opts.get('outgoing')
464 outg = opts.get('outgoing')
465 cont = opts.get('continue')
465 cont = opts.get('continue')
466 abort = opts.get('abort')
466 abort = opts.get('abort')
467 force = opts.get('force')
467 force = opts.get('force')
468 rules = opts.get('commands', '')
468 rules = opts.get('commands', '')
469 revs = opts.get('rev', [])
469 revs = opts.get('rev', [])
470 goal = 'new' # This invocation goal, in new, continue, abort
470 goal = 'new' # This invocation goal, in new, continue, abort
471 if force and not outg:
471 if force and not outg:
472 raise util.Abort(_('--force only allowed with --outgoing'))
472 raise util.Abort(_('--force only allowed with --outgoing'))
473 if cont:
473 if cont:
474 if util.any((outg, abort, revs, freeargs, rules)):
474 if util.any((outg, abort, revs, freeargs, rules)):
475 raise util.Abort(_('no arguments allowed with --continue'))
475 raise util.Abort(_('no arguments allowed with --continue'))
476 goal = 'continue'
476 goal = 'continue'
477 elif abort:
477 elif abort:
478 if util.any((outg, revs, freeargs, rules)):
478 if util.any((outg, revs, freeargs, rules)):
479 raise util.Abort(_('no arguments allowed with --abort'))
479 raise util.Abort(_('no arguments allowed with --abort'))
480 goal = 'abort'
480 goal = 'abort'
481 else:
481 else:
482 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
482 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
483 raise util.Abort(_('history edit already in progress, try '
483 raise util.Abort(_('history edit already in progress, try '
484 '--continue or --abort'))
484 '--continue or --abort'))
485 if outg:
485 if outg:
486 if revs:
486 if revs:
487 raise util.Abort(_('no revisions allowed with --outgoing'))
487 raise util.Abort(_('no revisions allowed with --outgoing'))
488 if len(freeargs) > 1:
488 if len(freeargs) > 1:
489 raise util.Abort(
489 raise util.Abort(
490 _('only one repo argument allowed with --outgoing'))
490 _('only one repo argument allowed with --outgoing'))
491 else:
491 else:
492 revs.extend(freeargs)
492 revs.extend(freeargs)
493 if len(revs) != 1:
493 if len(revs) != 1:
494 raise util.Abort(
494 raise util.Abort(
495 _('histedit requires exactly one parent revision'))
495 _('histedit requires exactly one parent revision'))
496
496
497
497
498 if goal == 'continue':
498 if goal == 'continue':
499 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
499 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
500 currentparent, wantnull = repo.dirstate.parents()
500 currentparent, wantnull = repo.dirstate.parents()
501 parentctx = repo[parentctxnode]
501 parentctx = repo[parentctxnode]
502 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
502 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
503 replacements.extend(repl)
503 replacements.extend(repl)
504 elif goal == 'abort':
504 elif goal == 'abort':
505 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
505 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
506 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
506 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
507 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
507 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
508 hg.clean(repo, topmost)
508 hg.clean(repo, topmost)
509 cleanupnode(ui, repo, 'created', tmpnodes)
509 cleanupnode(ui, repo, 'created', tmpnodes)
510 cleanupnode(ui, repo, 'temp', leafs)
510 cleanupnode(ui, repo, 'temp', leafs)
511 os.unlink(os.path.join(repo.path, 'histedit-state'))
511 os.unlink(os.path.join(repo.path, 'histedit-state'))
512 return
512 return
513 else:
513 else:
514 cmdutil.bailifchanged(repo)
514 cmdutil.bailifchanged(repo)
515
515
516 topmost, empty = repo.dirstate.parents()
516 topmost, empty = repo.dirstate.parents()
517 if outg:
517 if outg:
518 if freeargs:
518 if freeargs:
519 remote = freeargs[0]
519 remote = freeargs[0]
520 else:
520 else:
521 remote = None
521 remote = None
522 root = findoutgoing(ui, repo, remote, force, opts)
522 root = findoutgoing(ui, repo, remote, force, opts)
523 else:
523 else:
524 root = revs[0]
524 root = revs[0]
525 root = scmutil.revsingle(repo, root).node()
525 root = scmutil.revsingle(repo, root).node()
526
526
527 keep = opts.get('keep', False)
527 keep = opts.get('keep', False)
528 revs = between(repo, root, topmost, keep)
528 revs = between(repo, root, topmost, keep)
529 if not revs:
529 if not revs:
530 raise util.Abort(_('%s is not an ancestor of working directory') %
530 raise util.Abort(_('%s is not an ancestor of working directory') %
531 node.short(root))
531 node.short(root))
532
532
533 ctxs = [repo[r] for r in revs]
533 ctxs = [repo[r] for r in revs]
534 if not rules:
534 if not rules:
535 rules = '\n'.join([makedesc(c) for c in ctxs])
535 rules = '\n'.join([makedesc(c) for c in ctxs])
536 rules += '\n\n'
536 rules += '\n\n'
537 rules += editcomment % (node.short(root), node.short(topmost))
537 rules += editcomment % (node.short(root), node.short(topmost))
538 rules = ui.edit(rules, ui.username())
538 rules = ui.edit(rules, ui.username())
539 # Save edit rules in .hg/histedit-last-edit.txt in case
539 # Save edit rules in .hg/histedit-last-edit.txt in case
540 # the user needs to ask for help after something
540 # the user needs to ask for help after something
541 # surprising happens.
541 # surprising happens.
542 f = open(repo.join('histedit-last-edit.txt'), 'w')
542 f = open(repo.join('histedit-last-edit.txt'), 'w')
543 f.write(rules)
543 f.write(rules)
544 f.close()
544 f.close()
545 else:
545 else:
546 if rules == '-':
546 if rules == '-':
547 f = sys.stdin
547 f = sys.stdin
548 else:
548 else:
549 f = open(rules)
549 f = open(rules)
550 rules = f.read()
550 rules = f.read()
551 f.close()
551 f.close()
552 rules = [l for l in (r.strip() for r in rules.splitlines())
552 rules = [l for l in (r.strip() for r in rules.splitlines())
553 if l and not l[0] == '#']
553 if l and not l[0] == '#']
554 rules = verifyrules(rules, repo, ctxs)
554 rules = verifyrules(rules, repo, ctxs)
555
555
556 parentctx = repo[root].parents()[0]
556 parentctx = repo[root].parents()[0]
557 keep = opts.get('keep', False)
557 keep = opts.get('keep', False)
558 replacements = []
558 replacements = []
559
559
560
560
561 while rules:
561 while rules:
562 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
562 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
563 action, ha = rules.pop(0)
563 action, ha = rules.pop(0)
564 ui.debug('histedit: processing %s %s\n' % (action, ha))
564 ui.debug('histedit: processing %s %s\n' % (action, ha))
565 actfunc = actiontable[action]
565 actfunc = actiontable[action]
566 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
566 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
567 replacements.extend(replacement_)
567 replacements.extend(replacement_)
568
568
569 hg.update(repo, parentctx.node())
569 hg.update(repo, parentctx.node())
570
570
571 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
571 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
572 if mapping:
572 if mapping:
573 for prec, succs in mapping.iteritems():
573 for prec, succs in mapping.iteritems():
574 if not succs:
574 if not succs:
575 ui.debug('histedit: %s is dropped\n' % node.short(prec))
575 ui.debug('histedit: %s is dropped\n' % node.short(prec))
576 else:
576 else:
577 ui.debug('histedit: %s is replaced by %s\n' % (
577 ui.debug('histedit: %s is replaced by %s\n' % (
578 node.short(prec), node.short(succs[0])))
578 node.short(prec), node.short(succs[0])))
579 if len(succs) > 1:
579 if len(succs) > 1:
580 m = 'histedit: %s'
580 m = 'histedit: %s'
581 for n in succs[1:]:
581 for n in succs[1:]:
582 ui.debug(m % node.short(n))
582 ui.debug(m % node.short(n))
583
583
584 if not keep:
584 if not keep:
585 if mapping:
585 if mapping:
586 movebookmarks(ui, repo, mapping, topmost, ntm)
586 movebookmarks(ui, repo, mapping, topmost, ntm)
587 # TODO update mq state
587 # TODO update mq state
588 if obsolete._enabled:
588 if obsolete._enabled:
589 markers = []
589 markers = []
590 # sort by revision number because it sound "right"
590 # sort by revision number because it sound "right"
591 for prec in sorted(mapping, key=repo.changelog.rev):
591 for prec in sorted(mapping, key=repo.changelog.rev):
592 succs = mapping[prec]
592 succs = mapping[prec]
593 markers.append((repo[prec],
593 markers.append((repo[prec],
594 tuple(repo[s] for s in succs)))
594 tuple(repo[s] for s in succs)))
595 if markers:
595 if markers:
596 obsolete.createmarkers(repo, markers)
596 obsolete.createmarkers(repo, markers)
597 else:
597 else:
598 cleanupnode(ui, repo, 'replaced', mapping)
598 cleanupnode(ui, repo, 'replaced', mapping)
599
599
600 cleanupnode(ui, repo, 'temp', tmpnodes)
600 cleanupnode(ui, repo, 'temp', tmpnodes)
601 os.unlink(os.path.join(repo.path, 'histedit-state'))
601 os.unlink(os.path.join(repo.path, 'histedit-state'))
602 if os.path.exists(repo.sjoin('undo')):
602 if os.path.exists(repo.sjoin('undo')):
603 os.unlink(repo.sjoin('undo'))
603 os.unlink(repo.sjoin('undo'))
604
604
605
605
606 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
606 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
607 action, currentnode = rules.pop(0)
607 action, currentnode = rules.pop(0)
608 ctx = repo[currentnode]
608 ctx = repo[currentnode]
609 # is there any new commit between the expected parent and "."
609 # is there any new commit between the expected parent and "."
610 #
610 #
611 # note: does not take non linear new change in account (but previous
611 # note: does not take non linear new change in account (but previous
612 # implementation didn't used them anyway (issue3655)
612 # implementation didn't used them anyway (issue3655)
613 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
613 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
614 if parentctx.node() != node.nullid:
614 if parentctx.node() != node.nullid:
615 if not newchildren:
615 if not newchildren:
616 # `parentctxnode` should match but no result. This means that
616 # `parentctxnode` should match but no result. This means that
617 # currentnode is not a descendant from parentctxnode.
617 # currentnode is not a descendant from parentctxnode.
618 msg = _('%s is not an ancestor of working directory')
618 msg = _('%s is not an ancestor of working directory')
619 hint = _('update to %s or descendant and run "hg histedit '
619 hint = _('update to %s or descendant and run "hg histedit '
620 '--continue" again') % parentctx
620 '--continue" again') % parentctx
621 raise util.Abort(msg % parentctx, hint=hint)
621 raise util.Abort(msg % parentctx, hint=hint)
622 newchildren.pop(0) # remove parentctxnode
622 newchildren.pop(0) # remove parentctxnode
623 # Commit dirty working directory if necessary
623 # Commit dirty working directory if necessary
624 new = None
624 new = None
625 m, a, r, d = repo.status()[:4]
625 m, a, r, d = repo.status()[:4]
626 if m or a or r or d:
626 if m or a or r or d:
627 # prepare the message for the commit to comes
627 # prepare the message for the commit to comes
628 if action in ('f', 'fold'):
628 if action in ('f', 'fold'):
629 message = 'fold-temp-revision %s' % currentnode
629 message = 'fold-temp-revision %s' % currentnode
630 else:
630 else:
631 message = ctx.description() + '\n'
631 message = ctx.description() + '\n'
632 if action in ('e', 'edit', 'm', 'mess'):
632 if action in ('e', 'edit', 'm', 'mess'):
633 editor = cmdutil.commitforceeditor
633 editor = cmdutil.commitforceeditor
634 else:
634 else:
635 editor = False
635 editor = False
636 commit = commitfuncfor(repo, ctx)
636 commit = commitfuncfor(repo, ctx)
637 new = commit(text=message, user=ctx.user(),
637 new = commit(text=message, user=ctx.user(),
638 date=ctx.date(), extra=ctx.extra(),
638 date=ctx.date(), extra=ctx.extra(),
639 editor=editor)
639 editor=editor)
640 if new is not None:
640 if new is not None:
641 newchildren.append(new)
641 newchildren.append(new)
642
642
643 replacements = []
643 replacements = []
644 # track replacements
644 # track replacements
645 if ctx.node() not in newchildren:
645 if ctx.node() not in newchildren:
646 # note: new children may be empty when the changeset is dropped.
646 # note: new children may be empty when the changeset is dropped.
647 # this happen e.g during conflicting pick where we revert content
647 # this happen e.g during conflicting pick where we revert content
648 # to parent.
648 # to parent.
649 replacements.append((ctx.node(), tuple(newchildren)))
649 replacements.append((ctx.node(), tuple(newchildren)))
650
650
651 if action in ('f', 'fold'):
651 if action in ('f', 'fold'):
652 if newchildren:
652 if newchildren:
653 # finalize fold operation if applicable
653 # finalize fold operation if applicable
654 if new is None:
654 if new is None:
655 new = newchildren[-1]
655 new = newchildren[-1]
656 else:
656 else:
657 newchildren.pop() # remove new from internal changes
657 newchildren.pop() # remove new from internal changes
658 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
658 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
659 newchildren)
659 newchildren)
660 replacements.extend(repl)
660 replacements.extend(repl)
661 else:
661 else:
662 # newchildren is empty if the fold did not result in any commit
662 # newchildren is empty if the fold did not result in any commit
663 # this happen when all folded change are discarded during the
663 # this happen when all folded change are discarded during the
664 # merge.
664 # merge.
665 replacements.append((ctx.node(), (parentctx.node(),)))
665 replacements.append((ctx.node(), (parentctx.node(),)))
666 elif newchildren:
666 elif newchildren:
667 # otherwise update "parentctx" before proceeding to further operation
667 # otherwise update "parentctx" before proceeding to further operation
668 parentctx = repo[newchildren[-1]]
668 parentctx = repo[newchildren[-1]]
669 return parentctx, replacements
669 return parentctx, replacements
670
670
671
671
672 def between(repo, old, new, keep):
672 def between(repo, old, new, keep):
673 """select and validate the set of revision to edit
673 """select and validate the set of revision to edit
674
674
675 When keep is false, the specified set can't have children."""
675 When keep is false, the specified set can't have children."""
676 ctxs = list(repo.set('%n::%n', old, new))
676 ctxs = list(repo.set('%n::%n', old, new))
677 if ctxs and not keep:
677 if ctxs and not keep:
678 if (not obsolete._enabled and
678 if (not obsolete._enabled and
679 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
679 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
680 raise util.Abort(_('cannot edit history that would orphan nodes'))
680 raise util.Abort(_('cannot edit history that would orphan nodes'))
681 root = ctxs[0] # list is already sorted by repo.set
681 root = ctxs[0] # list is already sorted by repo.set
682 if not root.phase():
682 if not root.phase():
683 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
683 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
684 return [c.node() for c in ctxs]
684 return [c.node() for c in ctxs]
685
685
686
686
687 def writestate(repo, parentnode, rules, keep, topmost, replacements):
687 def writestate(repo, parentnode, rules, keep, topmost, replacements):
688 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
688 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
689 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
689 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
690 fp.close()
690 fp.close()
691
691
692 def readstate(repo):
692 def readstate(repo):
693 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
693 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
694 """
694 """
695 fp = open(os.path.join(repo.path, 'histedit-state'))
695 fp = open(os.path.join(repo.path, 'histedit-state'))
696 return pickle.load(fp)
696 return pickle.load(fp)
697
697
698
698
699 def makedesc(c):
699 def makedesc(c):
700 """build a initial action line for a ctx `c`
700 """build a initial action line for a ctx `c`
701
701
702 line are in the form:
702 line are in the form:
703
703
704 pick <hash> <rev> <summary>
704 pick <hash> <rev> <summary>
705 """
705 """
706 summary = ''
706 summary = ''
707 if c.description():
707 if c.description():
708 summary = c.description().splitlines()[0]
708 summary = c.description().splitlines()[0]
709 line = 'pick %s %d %s' % (c, c.rev(), summary)
709 line = 'pick %s %d %s' % (c, c.rev(), summary)
710 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
710 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
711
711
712 def verifyrules(rules, repo, ctxs):
712 def verifyrules(rules, repo, ctxs):
713 """Verify that there exists exactly one edit rule per given changeset.
713 """Verify that there exists exactly one edit rule per given changeset.
714
714
715 Will abort if there are to many or too few rules, a malformed rule,
715 Will abort if there are to many or too few rules, a malformed rule,
716 or a rule on a changeset outside of the user-given range.
716 or a rule on a changeset outside of the user-given range.
717 """
717 """
718 parsed = []
718 parsed = []
719 expected = set(str(c) for c in ctxs)
719 expected = set(str(c) for c in ctxs)
720 seen = set()
720 seen = set()
721 for r in rules:
721 for r in rules:
722 if ' ' not in r:
722 if ' ' not in r:
723 raise util.Abort(_('malformed line "%s"') % r)
723 raise util.Abort(_('malformed line "%s"') % r)
724 action, rest = r.split(' ', 1)
724 action, rest = r.split(' ', 1)
725 ha = rest.strip().split(' ', 1)[0]
725 ha = rest.strip().split(' ', 1)[0]
726 try:
726 try:
727 ha = str(repo[ha]) # ensure its a short hash
727 ha = str(repo[ha]) # ensure its a short hash
728 except error.RepoError:
728 except error.RepoError:
729 raise util.Abort(_('unknown changeset %s listed') % ha)
729 raise util.Abort(_('unknown changeset %s listed') % ha)
730 if ha not in expected:
730 if ha not in expected:
731 raise util.Abort(
731 raise util.Abort(
732 _('may not use changesets other than the ones listed'))
732 _('may not use changesets other than the ones listed'))
733 if ha in seen:
733 if ha in seen:
734 raise util.Abort(_('duplicated command for changeset %s') % ha)
734 raise util.Abort(_('duplicated command for changeset %s') % ha)
735 seen.add(ha)
735 seen.add(ha)
736 if action not in actiontable:
736 if action not in actiontable:
737 raise util.Abort(_('unknown action "%s"') % action)
737 raise util.Abort(_('unknown action "%s"') % action)
738 parsed.append([action, ha])
738 parsed.append([action, ha])
739 missing = sorted(expected - seen) # sort to stabilize output
739 missing = sorted(expected - seen) # sort to stabilize output
740 if missing:
740 if missing:
741 raise util.Abort(_('missing rules for changeset %s') % missing[0],
741 raise util.Abort(_('missing rules for changeset %s') % missing[0],
742 hint=_('do you want to use the drop action?'))
742 hint=_('do you want to use the drop action?'))
743 return parsed
743 return parsed
744
744
745 def processreplacement(repo, replacements):
745 def processreplacement(repo, replacements):
746 """process the list of replacements to return
746 """process the list of replacements to return
747
747
748 1) the final mapping between original and created nodes
748 1) the final mapping between original and created nodes
749 2) the list of temporary node created by histedit
749 2) the list of temporary node created by histedit
750 3) the list of new commit created by histedit"""
750 3) the list of new commit created by histedit"""
751 allsuccs = set()
751 allsuccs = set()
752 replaced = set()
752 replaced = set()
753 fullmapping = {}
753 fullmapping = {}
754 # initialise basic set
754 # initialise basic set
755 # fullmapping record all operation recorded in replacement
755 # fullmapping record all operation recorded in replacement
756 for rep in replacements:
756 for rep in replacements:
757 allsuccs.update(rep[1])
757 allsuccs.update(rep[1])
758 replaced.add(rep[0])
758 replaced.add(rep[0])
759 fullmapping.setdefault(rep[0], set()).update(rep[1])
759 fullmapping.setdefault(rep[0], set()).update(rep[1])
760 new = allsuccs - replaced
760 new = allsuccs - replaced
761 tmpnodes = allsuccs & replaced
761 tmpnodes = allsuccs & replaced
762 # Reduce content fullmapping into direct relation between original nodes
762 # Reduce content fullmapping into direct relation between original nodes
763 # and final node created during history edition
763 # and final node created during history edition
764 # Dropped changeset are replaced by an empty list
764 # Dropped changeset are replaced by an empty list
765 toproceed = set(fullmapping)
765 toproceed = set(fullmapping)
766 final = {}
766 final = {}
767 while toproceed:
767 while toproceed:
768 for x in list(toproceed):
768 for x in list(toproceed):
769 succs = fullmapping[x]
769 succs = fullmapping[x]
770 for s in list(succs):
770 for s in list(succs):
771 if s in toproceed:
771 if s in toproceed:
772 # non final node with unknown closure
772 # non final node with unknown closure
773 # We can't process this now
773 # We can't process this now
774 break
774 break
775 elif s in final:
775 elif s in final:
776 # non final node, replace with closure
776 # non final node, replace with closure
777 succs.remove(s)
777 succs.remove(s)
778 succs.update(final[s])
778 succs.update(final[s])
779 else:
779 else:
780 final[x] = succs
780 final[x] = succs
781 toproceed.remove(x)
781 toproceed.remove(x)
782 # remove tmpnodes from final mapping
782 # remove tmpnodes from final mapping
783 for n in tmpnodes:
783 for n in tmpnodes:
784 del final[n]
784 del final[n]
785 # we expect all changes involved in final to exist in the repo
785 # we expect all changes involved in final to exist in the repo
786 # turn `final` into list (topologically sorted)
786 # turn `final` into list (topologically sorted)
787 nm = repo.changelog.nodemap
787 nm = repo.changelog.nodemap
788 for prec, succs in final.items():
788 for prec, succs in final.items():
789 final[prec] = sorted(succs, key=nm.get)
789 final[prec] = sorted(succs, key=nm.get)
790
790
791 # computed topmost element (necessary for bookmark)
791 # computed topmost element (necessary for bookmark)
792 if new:
792 if new:
793 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
793 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
794 elif not final:
794 elif not final:
795 # Nothing rewritten at all. we won't need `newtopmost`
795 # Nothing rewritten at all. we won't need `newtopmost`
796 # It is the same as `oldtopmost` and `processreplacement` know it
796 # It is the same as `oldtopmost` and `processreplacement` know it
797 newtopmost = None
797 newtopmost = None
798 else:
798 else:
799 # every body died. The newtopmost is the parent of the root.
799 # every body died. The newtopmost is the parent of the root.
800 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
800 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
801
801
802 return final, tmpnodes, new, newtopmost
802 return final, tmpnodes, new, newtopmost
803
803
804 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
804 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
805 """Move bookmark from old to newly created node"""
805 """Move bookmark from old to newly created node"""
806 if not mapping:
806 if not mapping:
807 # if nothing got rewritten there is not purpose for this function
807 # if nothing got rewritten there is not purpose for this function
808 return
808 return
809 moves = []
809 moves = []
810 for bk, old in sorted(repo._bookmarks.iteritems()):
810 for bk, old in sorted(repo._bookmarks.iteritems()):
811 if old == oldtopmost:
811 if old == oldtopmost:
812 # special case ensure bookmark stay on tip.
812 # special case ensure bookmark stay on tip.
813 #
813 #
814 # This is arguably a feature and we may only want that for the
814 # This is arguably a feature and we may only want that for the
815 # active bookmark. But the behavior is kept compatible with the old
815 # active bookmark. But the behavior is kept compatible with the old
816 # version for now.
816 # version for now.
817 moves.append((bk, newtopmost))
817 moves.append((bk, newtopmost))
818 continue
818 continue
819 base = old
819 base = old
820 new = mapping.get(base, None)
820 new = mapping.get(base, None)
821 if new is None:
821 if new is None:
822 continue
822 continue
823 while not new:
823 while not new:
824 # base is killed, trying with parent
824 # base is killed, trying with parent
825 base = repo[base].p1().node()
825 base = repo[base].p1().node()
826 new = mapping.get(base, (base,))
826 new = mapping.get(base, (base,))
827 # nothing to move
827 # nothing to move
828 moves.append((bk, new[-1]))
828 moves.append((bk, new[-1]))
829 if moves:
829 if moves:
830 marks = repo._bookmarks
830 marks = repo._bookmarks
831 for mark, new in moves:
831 for mark, new in moves:
832 old = marks[mark]
832 old = marks[mark]
833 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
833 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
834 % (mark, node.short(old), node.short(new)))
834 % (mark, node.short(old), node.short(new)))
835 marks[mark] = new
835 marks[mark] = new
836 marks.write()
836 marks.write()
837
837
838 def cleanupnode(ui, repo, name, nodes):
838 def cleanupnode(ui, repo, name, nodes):
839 """strip a group of nodes from the repository
839 """strip a group of nodes from the repository
840
840
841 The set of node to strip may contains unknown nodes."""
841 The set of node to strip may contains unknown nodes."""
842 ui.debug('should strip %s nodes %s\n' %
842 ui.debug('should strip %s nodes %s\n' %
843 (name, ', '.join([node.short(n) for n in nodes])))
843 (name, ', '.join([node.short(n) for n in nodes])))
844 lock = None
844 lock = None
845 try:
845 try:
846 lock = repo.lock()
846 lock = repo.lock()
847 # Find all node that need to be stripped
847 # Find all node that need to be stripped
848 # (we hg %lr instead of %ln to silently ignore unknown item
848 # (we hg %lr instead of %ln to silently ignore unknown item
849 nm = repo.changelog.nodemap
849 nm = repo.changelog.nodemap
850 nodes = [n for n in nodes if n in nm]
850 nodes = [n for n in nodes if n in nm]
851 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
851 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
852 for c in roots:
852 for c in roots:
853 # We should process node in reverse order to strip tip most first.
853 # We should process node in reverse order to strip tip most first.
854 # but this trigger a bug in changegroup hook.
854 # but this trigger a bug in changegroup hook.
855 # This would reduce bundle overhead
855 # This would reduce bundle overhead
856 repair.strip(ui, repo, c)
856 repair.strip(ui, repo, c)
857 finally:
857 finally:
858 lockmod.release(lock)
858 lockmod.release(lock)
859
860 def summaryhook(ui, repo):
861 if not os.path.exists(repo.join('histedit-state')):
862 return
863 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
864 if rules:
865 # i18n: column positioning for "hg summary"
866 ui.write(_('hist: %s (histedit --continue)\n') %
867 (ui.label(_('%d remaining'), 'histedit.remaining') %
868 len(rules)))
869
870 def extsetup(ui):
871 cmdutil.summaryhooks.add('histedit', summaryhook)
@@ -1,229 +1,230 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > graphlog=
5 > graphlog=
6 > histedit=
6 > histedit=
7 > EOF
7 > EOF
8
8
9 $ initrepo ()
9 $ initrepo ()
10 > {
10 > {
11 > hg init r
11 > hg init r
12 > cd r
12 > cd r
13 > for x in a b c d e f ; do
13 > for x in a b c d e f ; do
14 > echo $x > $x
14 > echo $x > $x
15 > hg add $x
15 > hg add $x
16 > hg ci -m $x
16 > hg ci -m $x
17 > done
17 > done
18 > }
18 > }
19
19
20 $ initrepo
20 $ initrepo
21
21
22 log before edit
22 log before edit
23 $ hg log --graph
23 $ hg log --graph
24 @ changeset: 5:652413bf663e
24 @ changeset: 5:652413bf663e
25 | tag: tip
25 | tag: tip
26 | user: test
26 | user: test
27 | date: Thu Jan 01 00:00:00 1970 +0000
27 | date: Thu Jan 01 00:00:00 1970 +0000
28 | summary: f
28 | summary: f
29 |
29 |
30 o changeset: 4:e860deea161a
30 o changeset: 4:e860deea161a
31 | user: test
31 | user: test
32 | date: Thu Jan 01 00:00:00 1970 +0000
32 | date: Thu Jan 01 00:00:00 1970 +0000
33 | summary: e
33 | summary: e
34 |
34 |
35 o changeset: 3:055a42cdd887
35 o changeset: 3:055a42cdd887
36 | user: test
36 | user: test
37 | date: Thu Jan 01 00:00:00 1970 +0000
37 | date: Thu Jan 01 00:00:00 1970 +0000
38 | summary: d
38 | summary: d
39 |
39 |
40 o changeset: 2:177f92b77385
40 o changeset: 2:177f92b77385
41 | user: test
41 | user: test
42 | date: Thu Jan 01 00:00:00 1970 +0000
42 | date: Thu Jan 01 00:00:00 1970 +0000
43 | summary: c
43 | summary: c
44 |
44 |
45 o changeset: 1:d2ae7f538514
45 o changeset: 1:d2ae7f538514
46 | user: test
46 | user: test
47 | date: Thu Jan 01 00:00:00 1970 +0000
47 | date: Thu Jan 01 00:00:00 1970 +0000
48 | summary: b
48 | summary: b
49 |
49 |
50 o changeset: 0:cb9a9f314b8b
50 o changeset: 0:cb9a9f314b8b
51 user: test
51 user: test
52 date: Thu Jan 01 00:00:00 1970 +0000
52 date: Thu Jan 01 00:00:00 1970 +0000
53 summary: a
53 summary: a
54
54
55
55
56 edit the history
56 edit the history
57 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
57 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
58 > pick 177f92b77385 c
58 > pick 177f92b77385 c
59 > pick 055a42cdd887 d
59 > pick 055a42cdd887 d
60 > edit e860deea161a e
60 > edit e860deea161a e
61 > pick 652413bf663e f
61 > pick 652413bf663e f
62 > EOF
62 > EOF
63 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
63 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
64 Make changes as needed, you may commit or record as needed now.
64 Make changes as needed, you may commit or record as needed now.
65 When you are finished, run hg histedit --continue to resume.
65 When you are finished, run hg histedit --continue to resume.
66
66
67 Go at a random point and try to continue
67 Go at a random point and try to continue
68
68
69 $ hg id -n
69 $ hg id -n
70 3+
70 3+
71 $ hg up 0
71 $ hg up 0
72 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
72 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
73 $ HGEDITOR='echo foobaz > ' hg histedit --continue
73 $ HGEDITOR='echo foobaz > ' hg histedit --continue
74 abort: 055a42cdd887 is not an ancestor of working directory
74 abort: 055a42cdd887 is not an ancestor of working directory
75 (update to 055a42cdd887 or descendant and run "hg histedit --continue" again)
75 (update to 055a42cdd887 or descendant and run "hg histedit --continue" again)
76 [255]
76 [255]
77 $ hg up 3
77 $ hg up 3
78 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
79
79
80 commit, then edit the revision
80 commit, then edit the revision
81 $ hg ci -m 'wat'
81 $ hg ci -m 'wat'
82 created new head
82 created new head
83 $ echo a > e
83 $ echo a > e
84 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
84 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
85 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
85 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
86 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
86 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
87
87
88 $ hg log --graph
88 $ hg log --graph
89 @ changeset: 6:b5f70786f9b0
89 @ changeset: 6:b5f70786f9b0
90 | tag: tip
90 | tag: tip
91 | user: test
91 | user: test
92 | date: Thu Jan 01 00:00:00 1970 +0000
92 | date: Thu Jan 01 00:00:00 1970 +0000
93 | summary: f
93 | summary: f
94 |
94 |
95 o changeset: 5:a5e1ba2f7afb
95 o changeset: 5:a5e1ba2f7afb
96 | user: test
96 | user: test
97 | date: Thu Jan 01 00:00:00 1970 +0000
97 | date: Thu Jan 01 00:00:00 1970 +0000
98 | summary: foobaz
98 | summary: foobaz
99 |
99 |
100 o changeset: 4:1a60820cd1f6
100 o changeset: 4:1a60820cd1f6
101 | user: test
101 | user: test
102 | date: Thu Jan 01 00:00:00 1970 +0000
102 | date: Thu Jan 01 00:00:00 1970 +0000
103 | summary: wat
103 | summary: wat
104 |
104 |
105 o changeset: 3:055a42cdd887
105 o changeset: 3:055a42cdd887
106 | user: test
106 | user: test
107 | date: Thu Jan 01 00:00:00 1970 +0000
107 | date: Thu Jan 01 00:00:00 1970 +0000
108 | summary: d
108 | summary: d
109 |
109 |
110 o changeset: 2:177f92b77385
110 o changeset: 2:177f92b77385
111 | user: test
111 | user: test
112 | date: Thu Jan 01 00:00:00 1970 +0000
112 | date: Thu Jan 01 00:00:00 1970 +0000
113 | summary: c
113 | summary: c
114 |
114 |
115 o changeset: 1:d2ae7f538514
115 o changeset: 1:d2ae7f538514
116 | user: test
116 | user: test
117 | date: Thu Jan 01 00:00:00 1970 +0000
117 | date: Thu Jan 01 00:00:00 1970 +0000
118 | summary: b
118 | summary: b
119 |
119 |
120 o changeset: 0:cb9a9f314b8b
120 o changeset: 0:cb9a9f314b8b
121 user: test
121 user: test
122 date: Thu Jan 01 00:00:00 1970 +0000
122 date: Thu Jan 01 00:00:00 1970 +0000
123 summary: a
123 summary: a
124
124
125
125
126 $ hg cat e
126 $ hg cat e
127 a
127 a
128
128
129 check histedit_source
129 check histedit_source
130
130
131 $ hg log --debug --rev 5
131 $ hg log --debug --rev 5
132 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
132 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
133 phase: draft
133 phase: draft
134 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
134 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
135 parent: -1:0000000000000000000000000000000000000000
135 parent: -1:0000000000000000000000000000000000000000
136 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
136 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
137 user: test
137 user: test
138 date: Thu Jan 01 00:00:00 1970 +0000
138 date: Thu Jan 01 00:00:00 1970 +0000
139 files: e
139 files: e
140 extra: branch=default
140 extra: branch=default
141 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
141 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
142 description:
142 description:
143 foobaz
143 foobaz
144
144
145
145
146
146
147 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
147 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
148 > edit b5f70786f9b0 f
148 > edit b5f70786f9b0 f
149 > EOF
149 > EOF
150 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
150 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
151 Make changes as needed, you may commit or record as needed now.
151 Make changes as needed, you may commit or record as needed now.
152 When you are finished, run hg histedit --continue to resume.
152 When you are finished, run hg histedit --continue to resume.
153 $ hg status
153 $ hg status
154 A f
154 A f
155
155
156 $ hg summary
156 $ hg summary
157 parent: 5:a5e1ba2f7afb
157 parent: 5:a5e1ba2f7afb
158 foobaz
158 foobaz
159 branch: default
159 branch: default
160 commit: 1 added (new branch head)
160 commit: 1 added (new branch head)
161 update: 1 new changesets (update)
161 update: 1 new changesets (update)
162 hist: 1 remaining (histedit --continue)
162
163
163 $ HGEDITOR='true' hg histedit --continue
164 $ HGEDITOR='true' hg histedit --continue
164 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
165 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
165 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-backup.hg (glob)
166 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-backup.hg (glob)
166
167
167 $ hg status
168 $ hg status
168
169
169 log after edit
170 log after edit
170 $ hg log --limit 1
171 $ hg log --limit 1
171 changeset: 6:a107ee126658
172 changeset: 6:a107ee126658
172 tag: tip
173 tag: tip
173 user: test
174 user: test
174 date: Thu Jan 01 00:00:00 1970 +0000
175 date: Thu Jan 01 00:00:00 1970 +0000
175 summary: f
176 summary: f
176
177
177
178
178 say we'll change the message, but don't.
179 say we'll change the message, but don't.
179 $ cat > ../edit.sh <<EOF
180 $ cat > ../edit.sh <<EOF
180 > cat "\$1" | sed s/pick/mess/ > tmp
181 > cat "\$1" | sed s/pick/mess/ > tmp
181 > mv tmp "\$1"
182 > mv tmp "\$1"
182 > EOF
183 > EOF
183 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
184 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
184 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
185 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
185 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
186 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
186 $ hg status
187 $ hg status
187 $ hg log --limit 1
188 $ hg log --limit 1
188 changeset: 6:1fd3b2fe7754
189 changeset: 6:1fd3b2fe7754
189 tag: tip
190 tag: tip
190 user: test
191 user: test
191 date: Thu Jan 01 00:00:00 1970 +0000
192 date: Thu Jan 01 00:00:00 1970 +0000
192 summary: f
193 summary: f
193
194
194
195
195 modify the message
196 modify the message
196 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
197 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
197 > mess 1fd3b2fe7754 f
198 > mess 1fd3b2fe7754 f
198 > EOF
199 > EOF
199 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
200 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
200 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
201 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
201 $ hg status
202 $ hg status
202 $ hg log --limit 1
203 $ hg log --limit 1
203 changeset: 6:62feedb1200e
204 changeset: 6:62feedb1200e
204 tag: tip
205 tag: tip
205 user: test
206 user: test
206 date: Thu Jan 01 00:00:00 1970 +0000
207 date: Thu Jan 01 00:00:00 1970 +0000
207 summary: f
208 summary: f
208
209
209
210
210 rollback should not work after a histedit
211 rollback should not work after a histedit
211 $ hg rollback
212 $ hg rollback
212 no rollback information available
213 no rollback information available
213 [1]
214 [1]
214
215
215 $ cd ..
216 $ cd ..
216 $ hg clone -qr0 r r0
217 $ hg clone -qr0 r r0
217 $ cd r0
218 $ cd r0
218 $ hg phase -fdr0
219 $ hg phase -fdr0
219 $ hg histedit --commands - 0 2>&1 << EOF
220 $ hg histedit --commands - 0 2>&1 << EOF
220 > edit cb9a9f314b8b a > $EDITED
221 > edit cb9a9f314b8b a > $EDITED
221 > EOF
222 > EOF
222 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
223 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
223 adding a
224 adding a
224 Make changes as needed, you may commit or record as needed now.
225 Make changes as needed, you may commit or record as needed now.
225 When you are finished, run hg histedit --continue to resume.
226 When you are finished, run hg histedit --continue to resume.
226 [1]
227 [1]
227 $ HGEDITOR=true hg histedit --continue
228 $ HGEDITOR=true hg histedit --continue
228 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
229 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
229 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-backup.hg (glob)
230 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-backup.hg (glob)
General Comments 0
You need to be logged in to leave comments. Login now