##// END OF EJS Templates
patchbomb: add label and color to the confirm output...
Pierre-Yves David -
r23173:122f5c3f default
parent child Browse files
Show More
@@ -1,640 +1,645 b''
1 # color.py color output for Mercurial commands
1 # color.py color output for Mercurial 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 The color extension colorizes output from several Mercurial commands.
10 The color extension colorizes output from several Mercurial commands.
11 For example, the diff command shows additions in green and deletions
11 For example, the diff command shows additions in green and deletions
12 in red, while the status command shows modified files in magenta. Many
12 in red, while the status command shows modified files in magenta. Many
13 other commands have analogous colors. It is possible to customize
13 other commands have analogous colors. It is possible to customize
14 these colors.
14 these colors.
15
15
16 Effects
16 Effects
17 -------
17 -------
18
18
19 Other effects in addition to color, like bold and underlined text, are
19 Other effects in addition to color, like bold and underlined text, are
20 also available. By default, the terminfo database is used to find the
20 also available. By default, the terminfo database is used to find the
21 terminal codes used to change color and effect. If terminfo is not
21 terminal codes used to change color and effect. If terminfo is not
22 available, then effects are rendered with the ECMA-48 SGR control
22 available, then effects are rendered with the ECMA-48 SGR control
23 function (aka ANSI escape codes).
23 function (aka ANSI escape codes).
24
24
25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
28 'underline'. How each is rendered depends on the terminal emulator.
28 'underline'. How each is rendered depends on the terminal emulator.
29 Some may not be available for a given terminal type, and will be
29 Some may not be available for a given terminal type, and will be
30 silently ignored.
30 silently ignored.
31
31
32 Labels
32 Labels
33 ------
33 ------
34
34
35 Text receives color effects depending on the labels that it has. Many
35 Text receives color effects depending on the labels that it has. Many
36 default Mercurial commands emit labelled text. You can also define
36 default Mercurial commands emit labelled text. You can also define
37 your own labels in templates using the label function, see :hg:`help
37 your own labels in templates using the label function, see :hg:`help
38 templates`. A single portion of text may have more than one label. In
38 templates`. A single portion of text may have more than one label. In
39 that case, effects given to the last label will override any other
39 that case, effects given to the last label will override any other
40 effects. This includes the special "none" effect, which nullifies
40 effects. This includes the special "none" effect, which nullifies
41 other effects.
41 other effects.
42
42
43 Labels are normally invisible. In order to see these labels and their
43 Labels are normally invisible. In order to see these labels and their
44 position in the text, use the global --color=debug option. The same
44 position in the text, use the global --color=debug option. The same
45 anchor text may be associated to multiple labels, e.g.
45 anchor text may be associated to multiple labels, e.g.
46
46
47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
48
48
49 The following are the default effects for some default labels. Default
49 The following are the default effects for some default labels. Default
50 effects may be overridden from your configuration file::
50 effects may be overridden from your configuration file::
51
51
52 [color]
52 [color]
53 status.modified = blue bold underline red_background
53 status.modified = blue bold underline red_background
54 status.added = green bold
54 status.added = green bold
55 status.removed = red bold blue_background
55 status.removed = red bold blue_background
56 status.deleted = cyan bold underline
56 status.deleted = cyan bold underline
57 status.unknown = magenta bold underline
57 status.unknown = magenta bold underline
58 status.ignored = black bold
58 status.ignored = black bold
59
59
60 # 'none' turns off all effects
60 # 'none' turns off all effects
61 status.clean = none
61 status.clean = none
62 status.copied = none
62 status.copied = none
63
63
64 qseries.applied = blue bold underline
64 qseries.applied = blue bold underline
65 qseries.unapplied = black bold
65 qseries.unapplied = black bold
66 qseries.missing = red bold
66 qseries.missing = red bold
67
67
68 diff.diffline = bold
68 diff.diffline = bold
69 diff.extended = cyan bold
69 diff.extended = cyan bold
70 diff.file_a = red bold
70 diff.file_a = red bold
71 diff.file_b = green bold
71 diff.file_b = green bold
72 diff.hunk = magenta
72 diff.hunk = magenta
73 diff.deleted = red
73 diff.deleted = red
74 diff.inserted = green
74 diff.inserted = green
75 diff.changed = white
75 diff.changed = white
76 diff.tab =
76 diff.tab =
77 diff.trailingwhitespace = bold red_background
77 diff.trailingwhitespace = bold red_background
78
78
79 # Blank so it inherits the style of the surrounding label
79 # Blank so it inherits the style of the surrounding label
80 changeset.public =
80 changeset.public =
81 changeset.draft =
81 changeset.draft =
82 changeset.secret =
82 changeset.secret =
83
83
84 resolve.unresolved = red bold
84 resolve.unresolved = red bold
85 resolve.resolved = green bold
85 resolve.resolved = green bold
86
86
87 bookmarks.current = green
87 bookmarks.current = green
88
88
89 branches.active = none
89 branches.active = none
90 branches.closed = black bold
90 branches.closed = black bold
91 branches.current = green
91 branches.current = green
92 branches.inactive = none
92 branches.inactive = none
93
93
94 tags.normal = green
94 tags.normal = green
95 tags.local = black bold
95 tags.local = black bold
96
96
97 rebase.rebased = blue
97 rebase.rebased = blue
98 rebase.remaining = red bold
98 rebase.remaining = red bold
99
99
100 shelve.age = cyan
100 shelve.age = cyan
101 shelve.newest = green bold
101 shelve.newest = green bold
102 shelve.name = blue bold
102 shelve.name = blue bold
103
103
104 histedit.remaining = red bold
104 histedit.remaining = red bold
105
105
106 Custom colors
106 Custom colors
107 -------------
107 -------------
108
108
109 Because there are only eight standard colors, this module allows you
109 Because there are only eight standard colors, this module allows you
110 to define color names for other color slots which might be available
110 to define color names for other color slots which might be available
111 for your terminal type, assuming terminfo mode. For instance::
111 for your terminal type, assuming terminfo mode. For instance::
112
112
113 color.brightblue = 12
113 color.brightblue = 12
114 color.pink = 207
114 color.pink = 207
115 color.orange = 202
115 color.orange = 202
116
116
117 to set 'brightblue' to color slot 12 (useful for 16 color terminals
117 to set 'brightblue' to color slot 12 (useful for 16 color terminals
118 that have brighter colors defined in the upper eight) and, 'pink' and
118 that have brighter colors defined in the upper eight) and, 'pink' and
119 'orange' to colors in 256-color xterm's default color cube. These
119 'orange' to colors in 256-color xterm's default color cube. These
120 defined colors may then be used as any of the pre-defined eight,
120 defined colors may then be used as any of the pre-defined eight,
121 including appending '_background' to set the background to that color.
121 including appending '_background' to set the background to that color.
122
122
123 Modes
123 Modes
124 -----
124 -----
125
125
126 By default, the color extension will use ANSI mode (or win32 mode on
126 By default, the color extension will use ANSI mode (or win32 mode on
127 Windows) if it detects a terminal. To override auto mode (to enable
127 Windows) if it detects a terminal. To override auto mode (to enable
128 terminfo mode, for example), set the following configuration option::
128 terminfo mode, for example), set the following configuration option::
129
129
130 [color]
130 [color]
131 mode = terminfo
131 mode = terminfo
132
132
133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
134 disable color.
134 disable color.
135
135
136 Note that on some systems, terminfo mode may cause problems when using
136 Note that on some systems, terminfo mode may cause problems when using
137 color with the pager extension and less -R. less with the -R option
137 color with the pager extension and less -R. less with the -R option
138 will only display ECMA-48 color codes, and terminfo mode may sometimes
138 will only display ECMA-48 color codes, and terminfo mode may sometimes
139 emit codes that less doesn't understand. You can work around this by
139 emit codes that less doesn't understand. You can work around this by
140 either using ansi mode (or auto mode), or by using less -r (which will
140 either using ansi mode (or auto mode), or by using less -r (which will
141 pass through all terminal control codes, not just color control
141 pass through all terminal control codes, not just color control
142 codes).
142 codes).
143 '''
143 '''
144
144
145 import os
145 import os
146
146
147 from mercurial import cmdutil, commands, dispatch, extensions, ui as uimod, util
147 from mercurial import cmdutil, commands, dispatch, extensions, ui as uimod, util
148 from mercurial import templater, error
148 from mercurial import templater, error
149 from mercurial.i18n import _
149 from mercurial.i18n import _
150
150
151 cmdtable = {}
151 cmdtable = {}
152 command = cmdutil.command(cmdtable)
152 command = cmdutil.command(cmdtable)
153 testedwith = 'internal'
153 testedwith = 'internal'
154
154
155 # start and stop parameters for effects
155 # start and stop parameters for effects
156 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
156 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
157 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
157 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
158 'italic': 3, 'underline': 4, 'inverse': 7,
158 'italic': 3, 'underline': 4, 'inverse': 7,
159 'black_background': 40, 'red_background': 41,
159 'black_background': 40, 'red_background': 41,
160 'green_background': 42, 'yellow_background': 43,
160 'green_background': 42, 'yellow_background': 43,
161 'blue_background': 44, 'purple_background': 45,
161 'blue_background': 44, 'purple_background': 45,
162 'cyan_background': 46, 'white_background': 47}
162 'cyan_background': 46, 'white_background': 47}
163
163
164 def _terminfosetup(ui, mode):
164 def _terminfosetup(ui, mode):
165 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
165 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
166
166
167 global _terminfo_params
167 global _terminfo_params
168 # If we failed to load curses, we go ahead and return.
168 # If we failed to load curses, we go ahead and return.
169 if not _terminfo_params:
169 if not _terminfo_params:
170 return
170 return
171 # Otherwise, see what the config file says.
171 # Otherwise, see what the config file says.
172 if mode not in ('auto', 'terminfo'):
172 if mode not in ('auto', 'terminfo'):
173 return
173 return
174
174
175 _terminfo_params.update((key[6:], (False, int(val)))
175 _terminfo_params.update((key[6:], (False, int(val)))
176 for key, val in ui.configitems('color')
176 for key, val in ui.configitems('color')
177 if key.startswith('color.'))
177 if key.startswith('color.'))
178
178
179 try:
179 try:
180 curses.setupterm()
180 curses.setupterm()
181 except curses.error, e:
181 except curses.error, e:
182 _terminfo_params = {}
182 _terminfo_params = {}
183 return
183 return
184
184
185 for key, (b, e) in _terminfo_params.items():
185 for key, (b, e) in _terminfo_params.items():
186 if not b:
186 if not b:
187 continue
187 continue
188 if not curses.tigetstr(e):
188 if not curses.tigetstr(e):
189 # Most terminals don't support dim, invis, etc, so don't be
189 # Most terminals don't support dim, invis, etc, so don't be
190 # noisy and use ui.debug().
190 # noisy and use ui.debug().
191 ui.debug("no terminfo entry for %s\n" % e)
191 ui.debug("no terminfo entry for %s\n" % e)
192 del _terminfo_params[key]
192 del _terminfo_params[key]
193 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
193 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
194 # Only warn about missing terminfo entries if we explicitly asked for
194 # Only warn about missing terminfo entries if we explicitly asked for
195 # terminfo mode.
195 # terminfo mode.
196 if mode == "terminfo":
196 if mode == "terminfo":
197 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
197 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
198 "ECMA-48 color\n"))
198 "ECMA-48 color\n"))
199 _terminfo_params = {}
199 _terminfo_params = {}
200
200
201 def _modesetup(ui, coloropt):
201 def _modesetup(ui, coloropt):
202 global _terminfo_params
202 global _terminfo_params
203
203
204 if coloropt == 'debug':
204 if coloropt == 'debug':
205 return 'debug'
205 return 'debug'
206
206
207 auto = (coloropt == 'auto')
207 auto = (coloropt == 'auto')
208 always = not auto and util.parsebool(coloropt)
208 always = not auto and util.parsebool(coloropt)
209 if not always and not auto:
209 if not always and not auto:
210 return None
210 return None
211
211
212 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
212 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
213
213
214 mode = ui.config('color', 'mode', 'auto')
214 mode = ui.config('color', 'mode', 'auto')
215 realmode = mode
215 realmode = mode
216 if mode == 'auto':
216 if mode == 'auto':
217 if os.name == 'nt' and 'TERM' not in os.environ:
217 if os.name == 'nt' and 'TERM' not in os.environ:
218 # looks line a cmd.exe console, use win32 API or nothing
218 # looks line a cmd.exe console, use win32 API or nothing
219 realmode = 'win32'
219 realmode = 'win32'
220 else:
220 else:
221 realmode = 'ansi'
221 realmode = 'ansi'
222
222
223 if realmode == 'win32':
223 if realmode == 'win32':
224 _terminfo_params = {}
224 _terminfo_params = {}
225 if not w32effects:
225 if not w32effects:
226 if mode == 'win32':
226 if mode == 'win32':
227 # only warn if color.mode is explicitly set to win32
227 # only warn if color.mode is explicitly set to win32
228 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
228 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
229 return None
229 return None
230 _effects.update(w32effects)
230 _effects.update(w32effects)
231 elif realmode == 'ansi':
231 elif realmode == 'ansi':
232 _terminfo_params = {}
232 _terminfo_params = {}
233 elif realmode == 'terminfo':
233 elif realmode == 'terminfo':
234 _terminfosetup(ui, mode)
234 _terminfosetup(ui, mode)
235 if not _terminfo_params:
235 if not _terminfo_params:
236 if mode == 'terminfo':
236 if mode == 'terminfo':
237 ## FIXME Shouldn't we return None in this case too?
237 ## FIXME Shouldn't we return None in this case too?
238 # only warn if color.mode is explicitly set to win32
238 # only warn if color.mode is explicitly set to win32
239 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
239 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
240 realmode = 'ansi'
240 realmode = 'ansi'
241 else:
241 else:
242 return None
242 return None
243
243
244 if always or (auto and formatted):
244 if always or (auto and formatted):
245 return realmode
245 return realmode
246 return None
246 return None
247
247
248 try:
248 try:
249 import curses
249 import curses
250 # Mapping from effect name to terminfo attribute name or color number.
250 # Mapping from effect name to terminfo attribute name or color number.
251 # This will also force-load the curses module.
251 # This will also force-load the curses module.
252 _terminfo_params = {'none': (True, 'sgr0'),
252 _terminfo_params = {'none': (True, 'sgr0'),
253 'standout': (True, 'smso'),
253 'standout': (True, 'smso'),
254 'underline': (True, 'smul'),
254 'underline': (True, 'smul'),
255 'reverse': (True, 'rev'),
255 'reverse': (True, 'rev'),
256 'inverse': (True, 'rev'),
256 'inverse': (True, 'rev'),
257 'blink': (True, 'blink'),
257 'blink': (True, 'blink'),
258 'dim': (True, 'dim'),
258 'dim': (True, 'dim'),
259 'bold': (True, 'bold'),
259 'bold': (True, 'bold'),
260 'invisible': (True, 'invis'),
260 'invisible': (True, 'invis'),
261 'italic': (True, 'sitm'),
261 'italic': (True, 'sitm'),
262 'black': (False, curses.COLOR_BLACK),
262 'black': (False, curses.COLOR_BLACK),
263 'red': (False, curses.COLOR_RED),
263 'red': (False, curses.COLOR_RED),
264 'green': (False, curses.COLOR_GREEN),
264 'green': (False, curses.COLOR_GREEN),
265 'yellow': (False, curses.COLOR_YELLOW),
265 'yellow': (False, curses.COLOR_YELLOW),
266 'blue': (False, curses.COLOR_BLUE),
266 'blue': (False, curses.COLOR_BLUE),
267 'magenta': (False, curses.COLOR_MAGENTA),
267 'magenta': (False, curses.COLOR_MAGENTA),
268 'cyan': (False, curses.COLOR_CYAN),
268 'cyan': (False, curses.COLOR_CYAN),
269 'white': (False, curses.COLOR_WHITE)}
269 'white': (False, curses.COLOR_WHITE)}
270 except ImportError:
270 except ImportError:
271 _terminfo_params = {}
271 _terminfo_params = {}
272
272
273 _styles = {'grep.match': 'red bold',
273 _styles = {'grep.match': 'red bold',
274 'grep.linenumber': 'green',
274 'grep.linenumber': 'green',
275 'grep.rev': 'green',
275 'grep.rev': 'green',
276 'grep.change': 'green',
276 'grep.change': 'green',
277 'grep.sep': 'cyan',
277 'grep.sep': 'cyan',
278 'grep.filename': 'magenta',
278 'grep.filename': 'magenta',
279 'grep.user': 'magenta',
279 'grep.user': 'magenta',
280 'grep.date': 'magenta',
280 'grep.date': 'magenta',
281 'bookmarks.current': 'green',
281 'bookmarks.current': 'green',
282 'branches.active': 'none',
282 'branches.active': 'none',
283 'branches.closed': 'black bold',
283 'branches.closed': 'black bold',
284 'branches.current': 'green',
284 'branches.current': 'green',
285 'branches.inactive': 'none',
285 'branches.inactive': 'none',
286 'diff.changed': 'white',
286 'diff.changed': 'white',
287 'diff.deleted': 'red',
287 'diff.deleted': 'red',
288 'diff.diffline': 'bold',
288 'diff.diffline': 'bold',
289 'diff.extended': 'cyan bold',
289 'diff.extended': 'cyan bold',
290 'diff.file_a': 'red bold',
290 'diff.file_a': 'red bold',
291 'diff.file_b': 'green bold',
291 'diff.file_b': 'green bold',
292 'diff.hunk': 'magenta',
292 'diff.hunk': 'magenta',
293 'diff.inserted': 'green',
293 'diff.inserted': 'green',
294 'diff.tab': '',
294 'diff.tab': '',
295 'diff.trailingwhitespace': 'bold red_background',
295 'diff.trailingwhitespace': 'bold red_background',
296 'changeset.public' : '',
296 'changeset.public' : '',
297 'changeset.draft' : '',
297 'changeset.draft' : '',
298 'changeset.secret' : '',
298 'changeset.secret' : '',
299 'diffstat.deleted': 'red',
299 'diffstat.deleted': 'red',
300 'diffstat.inserted': 'green',
300 'diffstat.inserted': 'green',
301 'histedit.remaining': 'red bold',
301 'histedit.remaining': 'red bold',
302 'ui.prompt': 'yellow',
302 'ui.prompt': 'yellow',
303 'log.changeset': 'yellow',
303 'log.changeset': 'yellow',
304 'patchbomb.finalsummary': '',
305 'patchbomb.from': 'magenta',
306 'patchbomb.to': 'cyan',
307 'patchbomb.subject': 'green',
308 'patchbomb.diffstats': '',
304 'rebase.rebased': 'blue',
309 'rebase.rebased': 'blue',
305 'rebase.remaining': 'red bold',
310 'rebase.remaining': 'red bold',
306 'resolve.resolved': 'green bold',
311 'resolve.resolved': 'green bold',
307 'resolve.unresolved': 'red bold',
312 'resolve.unresolved': 'red bold',
308 'shelve.age': 'cyan',
313 'shelve.age': 'cyan',
309 'shelve.newest': 'green bold',
314 'shelve.newest': 'green bold',
310 'shelve.name': 'blue bold',
315 'shelve.name': 'blue bold',
311 'status.added': 'green bold',
316 'status.added': 'green bold',
312 'status.clean': 'none',
317 'status.clean': 'none',
313 'status.copied': 'none',
318 'status.copied': 'none',
314 'status.deleted': 'cyan bold underline',
319 'status.deleted': 'cyan bold underline',
315 'status.ignored': 'black bold',
320 'status.ignored': 'black bold',
316 'status.modified': 'blue bold',
321 'status.modified': 'blue bold',
317 'status.removed': 'red bold',
322 'status.removed': 'red bold',
318 'status.unknown': 'magenta bold underline',
323 'status.unknown': 'magenta bold underline',
319 'tags.normal': 'green',
324 'tags.normal': 'green',
320 'tags.local': 'black bold'}
325 'tags.local': 'black bold'}
321
326
322
327
323 def _effect_str(effect):
328 def _effect_str(effect):
324 '''Helper function for render_effects().'''
329 '''Helper function for render_effects().'''
325
330
326 bg = False
331 bg = False
327 if effect.endswith('_background'):
332 if effect.endswith('_background'):
328 bg = True
333 bg = True
329 effect = effect[:-11]
334 effect = effect[:-11]
330 attr, val = _terminfo_params[effect]
335 attr, val = _terminfo_params[effect]
331 if attr:
336 if attr:
332 return curses.tigetstr(val)
337 return curses.tigetstr(val)
333 elif bg:
338 elif bg:
334 return curses.tparm(curses.tigetstr('setab'), val)
339 return curses.tparm(curses.tigetstr('setab'), val)
335 else:
340 else:
336 return curses.tparm(curses.tigetstr('setaf'), val)
341 return curses.tparm(curses.tigetstr('setaf'), val)
337
342
338 def render_effects(text, effects):
343 def render_effects(text, effects):
339 'Wrap text in commands to turn on each effect.'
344 'Wrap text in commands to turn on each effect.'
340 if not text:
345 if not text:
341 return text
346 return text
342 if not _terminfo_params:
347 if not _terminfo_params:
343 start = [str(_effects[e]) for e in ['none'] + effects.split()]
348 start = [str(_effects[e]) for e in ['none'] + effects.split()]
344 start = '\033[' + ';'.join(start) + 'm'
349 start = '\033[' + ';'.join(start) + 'm'
345 stop = '\033[' + str(_effects['none']) + 'm'
350 stop = '\033[' + str(_effects['none']) + 'm'
346 else:
351 else:
347 start = ''.join(_effect_str(effect)
352 start = ''.join(_effect_str(effect)
348 for effect in ['none'] + effects.split())
353 for effect in ['none'] + effects.split())
349 stop = _effect_str('none')
354 stop = _effect_str('none')
350 return ''.join([start, text, stop])
355 return ''.join([start, text, stop])
351
356
352 def extstyles():
357 def extstyles():
353 for name, ext in extensions.extensions():
358 for name, ext in extensions.extensions():
354 _styles.update(getattr(ext, 'colortable', {}))
359 _styles.update(getattr(ext, 'colortable', {}))
355
360
356 def valideffect(effect):
361 def valideffect(effect):
357 'Determine if the effect is valid or not.'
362 'Determine if the effect is valid or not.'
358 good = False
363 good = False
359 if not _terminfo_params and effect in _effects:
364 if not _terminfo_params and effect in _effects:
360 good = True
365 good = True
361 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
366 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
362 good = True
367 good = True
363 return good
368 return good
364
369
365 def configstyles(ui):
370 def configstyles(ui):
366 for status, cfgeffects in ui.configitems('color'):
371 for status, cfgeffects in ui.configitems('color'):
367 if '.' not in status or status.startswith('color.'):
372 if '.' not in status or status.startswith('color.'):
368 continue
373 continue
369 cfgeffects = ui.configlist('color', status)
374 cfgeffects = ui.configlist('color', status)
370 if cfgeffects:
375 if cfgeffects:
371 good = []
376 good = []
372 for e in cfgeffects:
377 for e in cfgeffects:
373 if valideffect(e):
378 if valideffect(e):
374 good.append(e)
379 good.append(e)
375 else:
380 else:
376 ui.warn(_("ignoring unknown color/effect %r "
381 ui.warn(_("ignoring unknown color/effect %r "
377 "(configured in color.%s)\n")
382 "(configured in color.%s)\n")
378 % (e, status))
383 % (e, status))
379 _styles[status] = ' '.join(good)
384 _styles[status] = ' '.join(good)
380
385
381 class colorui(uimod.ui):
386 class colorui(uimod.ui):
382 def popbuffer(self, labeled=False):
387 def popbuffer(self, labeled=False):
383 if self._colormode is None:
388 if self._colormode is None:
384 return super(colorui, self).popbuffer(labeled)
389 return super(colorui, self).popbuffer(labeled)
385
390
386 self._bufferstates.pop()
391 self._bufferstates.pop()
387 if labeled:
392 if labeled:
388 return ''.join(self.label(a, label) for a, label
393 return ''.join(self.label(a, label) for a, label
389 in self._buffers.pop())
394 in self._buffers.pop())
390 return ''.join(a for a, label in self._buffers.pop())
395 return ''.join(a for a, label in self._buffers.pop())
391
396
392 _colormode = 'ansi'
397 _colormode = 'ansi'
393 def write(self, *args, **opts):
398 def write(self, *args, **opts):
394 if self._colormode is None:
399 if self._colormode is None:
395 return super(colorui, self).write(*args, **opts)
400 return super(colorui, self).write(*args, **opts)
396
401
397 label = opts.get('label', '')
402 label = opts.get('label', '')
398 if self._buffers:
403 if self._buffers:
399 self._buffers[-1].extend([(str(a), label) for a in args])
404 self._buffers[-1].extend([(str(a), label) for a in args])
400 elif self._colormode == 'win32':
405 elif self._colormode == 'win32':
401 for a in args:
406 for a in args:
402 win32print(a, super(colorui, self).write, **opts)
407 win32print(a, super(colorui, self).write, **opts)
403 else:
408 else:
404 return super(colorui, self).write(
409 return super(colorui, self).write(
405 *[self.label(str(a), label) for a in args], **opts)
410 *[self.label(str(a), label) for a in args], **opts)
406
411
407 def write_err(self, *args, **opts):
412 def write_err(self, *args, **opts):
408 if self._colormode is None:
413 if self._colormode is None:
409 return super(colorui, self).write_err(*args, **opts)
414 return super(colorui, self).write_err(*args, **opts)
410
415
411 label = opts.get('label', '')
416 label = opts.get('label', '')
412 if self._bufferstates and self._bufferstates[-1]:
417 if self._bufferstates and self._bufferstates[-1]:
413 return self.write(*args, **opts)
418 return self.write(*args, **opts)
414 if self._colormode == 'win32':
419 if self._colormode == 'win32':
415 for a in args:
420 for a in args:
416 win32print(a, super(colorui, self).write_err, **opts)
421 win32print(a, super(colorui, self).write_err, **opts)
417 else:
422 else:
418 return super(colorui, self).write_err(
423 return super(colorui, self).write_err(
419 *[self.label(str(a), label) for a in args], **opts)
424 *[self.label(str(a), label) for a in args], **opts)
420
425
421 def showlabel(self, msg, label):
426 def showlabel(self, msg, label):
422 if label and msg:
427 if label and msg:
423 if msg[-1] == '\n':
428 if msg[-1] == '\n':
424 return "[%s|%s]\n" % (label, msg[:-1])
429 return "[%s|%s]\n" % (label, msg[:-1])
425 else:
430 else:
426 return "[%s|%s]" % (label, msg)
431 return "[%s|%s]" % (label, msg)
427 else:
432 else:
428 return msg
433 return msg
429
434
430 def label(self, msg, label):
435 def label(self, msg, label):
431 if self._colormode is None:
436 if self._colormode is None:
432 return super(colorui, self).label(msg, label)
437 return super(colorui, self).label(msg, label)
433
438
434 if self._colormode == 'debug':
439 if self._colormode == 'debug':
435 return self.showlabel(msg, label)
440 return self.showlabel(msg, label)
436
441
437 effects = []
442 effects = []
438 for l in label.split():
443 for l in label.split():
439 s = _styles.get(l, '')
444 s = _styles.get(l, '')
440 if s:
445 if s:
441 effects.append(s)
446 effects.append(s)
442 elif valideffect(l):
447 elif valideffect(l):
443 effects.append(l)
448 effects.append(l)
444 effects = ' '.join(effects)
449 effects = ' '.join(effects)
445 if effects:
450 if effects:
446 return '\n'.join([render_effects(s, effects)
451 return '\n'.join([render_effects(s, effects)
447 for s in msg.split('\n')])
452 for s in msg.split('\n')])
448 return msg
453 return msg
449
454
450 def templatelabel(context, mapping, args):
455 def templatelabel(context, mapping, args):
451 if len(args) != 2:
456 if len(args) != 2:
452 # i18n: "label" is a keyword
457 # i18n: "label" is a keyword
453 raise error.ParseError(_("label expects two arguments"))
458 raise error.ParseError(_("label expects two arguments"))
454
459
455 # add known effects to the mapping so symbols like 'red', 'bold',
460 # add known effects to the mapping so symbols like 'red', 'bold',
456 # etc. don't need to be quoted
461 # etc. don't need to be quoted
457 mapping.update(dict([(k, k) for k in _effects]))
462 mapping.update(dict([(k, k) for k in _effects]))
458
463
459 thing = templater._evalifliteral(args[1], context, mapping)
464 thing = templater._evalifliteral(args[1], context, mapping)
460
465
461 # apparently, repo could be a string that is the favicon?
466 # apparently, repo could be a string that is the favicon?
462 repo = mapping.get('repo', '')
467 repo = mapping.get('repo', '')
463 if isinstance(repo, str):
468 if isinstance(repo, str):
464 return thing
469 return thing
465
470
466 label = templater._evalifliteral(args[0], context, mapping)
471 label = templater._evalifliteral(args[0], context, mapping)
467
472
468 thing = templater.stringify(thing)
473 thing = templater.stringify(thing)
469 label = templater.stringify(label)
474 label = templater.stringify(label)
470
475
471 return repo.ui.label(thing, label)
476 return repo.ui.label(thing, label)
472
477
473 def uisetup(ui):
478 def uisetup(ui):
474 if ui.plain():
479 if ui.plain():
475 return
480 return
476 if not isinstance(ui, colorui):
481 if not isinstance(ui, colorui):
477 colorui.__bases__ = (ui.__class__,)
482 colorui.__bases__ = (ui.__class__,)
478 ui.__class__ = colorui
483 ui.__class__ = colorui
479 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
484 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
480 mode = _modesetup(ui_, opts['color'])
485 mode = _modesetup(ui_, opts['color'])
481 colorui._colormode = mode
486 colorui._colormode = mode
482 if mode and mode != 'debug':
487 if mode and mode != 'debug':
483 extstyles()
488 extstyles()
484 configstyles(ui_)
489 configstyles(ui_)
485 return orig(ui_, opts, cmd, cmdfunc)
490 return orig(ui_, opts, cmd, cmdfunc)
486 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
491 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
487 templater.funcs['label'] = templatelabel
492 templater.funcs['label'] = templatelabel
488
493
489 def extsetup(ui):
494 def extsetup(ui):
490 commands.globalopts.append(
495 commands.globalopts.append(
491 ('', 'color', 'auto',
496 ('', 'color', 'auto',
492 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
497 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
493 # and should not be translated
498 # and should not be translated
494 _("when to colorize (boolean, always, auto, never, or debug)"),
499 _("when to colorize (boolean, always, auto, never, or debug)"),
495 _('TYPE')))
500 _('TYPE')))
496
501
497 @command('debugcolor', [], 'hg debugcolor')
502 @command('debugcolor', [], 'hg debugcolor')
498 def debugcolor(ui, repo, **opts):
503 def debugcolor(ui, repo, **opts):
499 global _styles
504 global _styles
500 _styles = {}
505 _styles = {}
501 for effect in _effects.keys():
506 for effect in _effects.keys():
502 _styles[effect] = effect
507 _styles[effect] = effect
503 ui.write(('color mode: %s\n') % ui._colormode)
508 ui.write(('color mode: %s\n') % ui._colormode)
504 ui.write(_('available colors:\n'))
509 ui.write(_('available colors:\n'))
505 for label, colors in _styles.items():
510 for label, colors in _styles.items():
506 ui.write(('%s\n') % colors, label=label)
511 ui.write(('%s\n') % colors, label=label)
507
512
508 if os.name != 'nt':
513 if os.name != 'nt':
509 w32effects = None
514 w32effects = None
510 else:
515 else:
511 import re, ctypes
516 import re, ctypes
512
517
513 _kernel32 = ctypes.windll.kernel32
518 _kernel32 = ctypes.windll.kernel32
514
519
515 _WORD = ctypes.c_ushort
520 _WORD = ctypes.c_ushort
516
521
517 _INVALID_HANDLE_VALUE = -1
522 _INVALID_HANDLE_VALUE = -1
518
523
519 class _COORD(ctypes.Structure):
524 class _COORD(ctypes.Structure):
520 _fields_ = [('X', ctypes.c_short),
525 _fields_ = [('X', ctypes.c_short),
521 ('Y', ctypes.c_short)]
526 ('Y', ctypes.c_short)]
522
527
523 class _SMALL_RECT(ctypes.Structure):
528 class _SMALL_RECT(ctypes.Structure):
524 _fields_ = [('Left', ctypes.c_short),
529 _fields_ = [('Left', ctypes.c_short),
525 ('Top', ctypes.c_short),
530 ('Top', ctypes.c_short),
526 ('Right', ctypes.c_short),
531 ('Right', ctypes.c_short),
527 ('Bottom', ctypes.c_short)]
532 ('Bottom', ctypes.c_short)]
528
533
529 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
534 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
530 _fields_ = [('dwSize', _COORD),
535 _fields_ = [('dwSize', _COORD),
531 ('dwCursorPosition', _COORD),
536 ('dwCursorPosition', _COORD),
532 ('wAttributes', _WORD),
537 ('wAttributes', _WORD),
533 ('srWindow', _SMALL_RECT),
538 ('srWindow', _SMALL_RECT),
534 ('dwMaximumWindowSize', _COORD)]
539 ('dwMaximumWindowSize', _COORD)]
535
540
536 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
541 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
537 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
542 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
538
543
539 _FOREGROUND_BLUE = 0x0001
544 _FOREGROUND_BLUE = 0x0001
540 _FOREGROUND_GREEN = 0x0002
545 _FOREGROUND_GREEN = 0x0002
541 _FOREGROUND_RED = 0x0004
546 _FOREGROUND_RED = 0x0004
542 _FOREGROUND_INTENSITY = 0x0008
547 _FOREGROUND_INTENSITY = 0x0008
543
548
544 _BACKGROUND_BLUE = 0x0010
549 _BACKGROUND_BLUE = 0x0010
545 _BACKGROUND_GREEN = 0x0020
550 _BACKGROUND_GREEN = 0x0020
546 _BACKGROUND_RED = 0x0040
551 _BACKGROUND_RED = 0x0040
547 _BACKGROUND_INTENSITY = 0x0080
552 _BACKGROUND_INTENSITY = 0x0080
548
553
549 _COMMON_LVB_REVERSE_VIDEO = 0x4000
554 _COMMON_LVB_REVERSE_VIDEO = 0x4000
550 _COMMON_LVB_UNDERSCORE = 0x8000
555 _COMMON_LVB_UNDERSCORE = 0x8000
551
556
552 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
557 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
553 w32effects = {
558 w32effects = {
554 'none': -1,
559 'none': -1,
555 'black': 0,
560 'black': 0,
556 'red': _FOREGROUND_RED,
561 'red': _FOREGROUND_RED,
557 'green': _FOREGROUND_GREEN,
562 'green': _FOREGROUND_GREEN,
558 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
563 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
559 'blue': _FOREGROUND_BLUE,
564 'blue': _FOREGROUND_BLUE,
560 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
565 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
561 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
566 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
562 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
567 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
563 'bold': _FOREGROUND_INTENSITY,
568 'bold': _FOREGROUND_INTENSITY,
564 'black_background': 0x100, # unused value > 0x0f
569 'black_background': 0x100, # unused value > 0x0f
565 'red_background': _BACKGROUND_RED,
570 'red_background': _BACKGROUND_RED,
566 'green_background': _BACKGROUND_GREEN,
571 'green_background': _BACKGROUND_GREEN,
567 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
572 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
568 'blue_background': _BACKGROUND_BLUE,
573 'blue_background': _BACKGROUND_BLUE,
569 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
574 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
570 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
575 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
571 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
576 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
572 _BACKGROUND_BLUE),
577 _BACKGROUND_BLUE),
573 'bold_background': _BACKGROUND_INTENSITY,
578 'bold_background': _BACKGROUND_INTENSITY,
574 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
579 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
575 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
580 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
576 }
581 }
577
582
578 passthrough = set([_FOREGROUND_INTENSITY,
583 passthrough = set([_FOREGROUND_INTENSITY,
579 _BACKGROUND_INTENSITY,
584 _BACKGROUND_INTENSITY,
580 _COMMON_LVB_UNDERSCORE,
585 _COMMON_LVB_UNDERSCORE,
581 _COMMON_LVB_REVERSE_VIDEO])
586 _COMMON_LVB_REVERSE_VIDEO])
582
587
583 stdout = _kernel32.GetStdHandle(
588 stdout = _kernel32.GetStdHandle(
584 _STD_OUTPUT_HANDLE) # don't close the handle returned
589 _STD_OUTPUT_HANDLE) # don't close the handle returned
585 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
590 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
586 w32effects = None
591 w32effects = None
587 else:
592 else:
588 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
593 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
589 if not _kernel32.GetConsoleScreenBufferInfo(
594 if not _kernel32.GetConsoleScreenBufferInfo(
590 stdout, ctypes.byref(csbi)):
595 stdout, ctypes.byref(csbi)):
591 # stdout may not support GetConsoleScreenBufferInfo()
596 # stdout may not support GetConsoleScreenBufferInfo()
592 # when called from subprocess or redirected
597 # when called from subprocess or redirected
593 w32effects = None
598 w32effects = None
594 else:
599 else:
595 origattr = csbi.wAttributes
600 origattr = csbi.wAttributes
596 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
601 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
597 re.MULTILINE | re.DOTALL)
602 re.MULTILINE | re.DOTALL)
598
603
599 def win32print(text, orig, **opts):
604 def win32print(text, orig, **opts):
600 label = opts.get('label', '')
605 label = opts.get('label', '')
601 attr = origattr
606 attr = origattr
602
607
603 def mapcolor(val, attr):
608 def mapcolor(val, attr):
604 if val == -1:
609 if val == -1:
605 return origattr
610 return origattr
606 elif val in passthrough:
611 elif val in passthrough:
607 return attr | val
612 return attr | val
608 elif val > 0x0f:
613 elif val > 0x0f:
609 return (val & 0x70) | (attr & 0x8f)
614 return (val & 0x70) | (attr & 0x8f)
610 else:
615 else:
611 return (val & 0x07) | (attr & 0xf8)
616 return (val & 0x07) | (attr & 0xf8)
612
617
613 # determine console attributes based on labels
618 # determine console attributes based on labels
614 for l in label.split():
619 for l in label.split():
615 style = _styles.get(l, '')
620 style = _styles.get(l, '')
616 for effect in style.split():
621 for effect in style.split():
617 try:
622 try:
618 attr = mapcolor(w32effects[effect], attr)
623 attr = mapcolor(w32effects[effect], attr)
619 except KeyError:
624 except KeyError:
620 # w32effects could not have certain attributes so we skip
625 # w32effects could not have certain attributes so we skip
621 # them if not found
626 # them if not found
622 pass
627 pass
623 # hack to ensure regexp finds data
628 # hack to ensure regexp finds data
624 if not text.startswith('\033['):
629 if not text.startswith('\033['):
625 text = '\033[m' + text
630 text = '\033[m' + text
626
631
627 # Look for ANSI-like codes embedded in text
632 # Look for ANSI-like codes embedded in text
628 m = re.match(ansire, text)
633 m = re.match(ansire, text)
629
634
630 try:
635 try:
631 while m:
636 while m:
632 for sattr in m.group(1).split(';'):
637 for sattr in m.group(1).split(';'):
633 if sattr:
638 if sattr:
634 attr = mapcolor(int(sattr), attr)
639 attr = mapcolor(int(sattr), attr)
635 _kernel32.SetConsoleTextAttribute(stdout, attr)
640 _kernel32.SetConsoleTextAttribute(stdout, attr)
636 orig(m.group(2), **opts)
641 orig(m.group(2), **opts)
637 m = re.match(ansire, m.group(3))
642 m = re.match(ansire, m.group(3))
638 finally:
643 finally:
639 # Explicitly reset original attributes
644 # Explicitly reset original attributes
640 _kernel32.SetConsoleTextAttribute(stdout, origattr)
645 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,575 +1,575 b''
1 # patchbomb.py - sending Mercurial changesets as patch emails
1 # patchbomb.py - sending Mercurial changesets as patch emails
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to send changesets as (a series of) patch emails
8 '''command to send changesets as (a series of) patch emails
9
9
10 The series is started off with a "[PATCH 0 of N]" introduction, which
10 The series is started off with a "[PATCH 0 of N]" introduction, which
11 describes the series as a whole.
11 describes the series as a whole.
12
12
13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
14 first line of the changeset description as the subject text. The
14 first line of the changeset description as the subject text. The
15 message contains two or three body parts:
15 message contains two or three body parts:
16
16
17 - The changeset description.
17 - The changeset description.
18 - [Optional] The result of running diffstat on the patch.
18 - [Optional] The result of running diffstat on the patch.
19 - The patch itself, as generated by :hg:`export`.
19 - The patch itself, as generated by :hg:`export`.
20
20
21 Each message refers to the first in the series using the In-Reply-To
21 Each message refers to the first in the series using the In-Reply-To
22 and References headers, so they will show up as a sequence in threaded
22 and References headers, so they will show up as a sequence in threaded
23 mail and news readers, and in mail archives.
23 mail and news readers, and in mail archives.
24
24
25 To configure other defaults, add a section like this to your
25 To configure other defaults, add a section like this to your
26 configuration file::
26 configuration file::
27
27
28 [email]
28 [email]
29 from = My Name <my@email>
29 from = My Name <my@email>
30 to = recipient1, recipient2, ...
30 to = recipient1, recipient2, ...
31 cc = cc1, cc2, ...
31 cc = cc1, cc2, ...
32 bcc = bcc1, bcc2, ...
32 bcc = bcc1, bcc2, ...
33 reply-to = address1, address2, ...
33 reply-to = address1, address2, ...
34
34
35 Use ``[patchbomb]`` as configuration section name if you need to
35 Use ``[patchbomb]`` as configuration section name if you need to
36 override global ``[email]`` address settings.
36 override global ``[email]`` address settings.
37
37
38 Then you can use the :hg:`email` command to mail a series of
38 Then you can use the :hg:`email` command to mail a series of
39 changesets as a patchbomb.
39 changesets as a patchbomb.
40
40
41 You can also either configure the method option in the email section
41 You can also either configure the method option in the email section
42 to be a sendmail compatible mailer or fill out the [smtp] section so
42 to be a sendmail compatible mailer or fill out the [smtp] section so
43 that the patchbomb extension can automatically send patchbombs
43 that the patchbomb extension can automatically send patchbombs
44 directly from the commandline. See the [email] and [smtp] sections in
44 directly from the commandline. See the [email] and [smtp] sections in
45 hgrc(5) for details.
45 hgrc(5) for details.
46 '''
46 '''
47
47
48 import os, errno, socket, tempfile, cStringIO
48 import os, errno, socket, tempfile, cStringIO
49 import email
49 import email
50 # On python2.4 you have to import these by name or they fail to
50 # On python2.4 you have to import these by name or they fail to
51 # load. This was not a problem on Python 2.7.
51 # load. This was not a problem on Python 2.7.
52 import email.Generator
52 import email.Generator
53 import email.MIMEMultipart
53 import email.MIMEMultipart
54
54
55 from mercurial import cmdutil, commands, hg, mail, patch, util
55 from mercurial import cmdutil, commands, hg, mail, patch, util
56 from mercurial import scmutil
56 from mercurial import scmutil
57 from mercurial.i18n import _
57 from mercurial.i18n import _
58 from mercurial.node import bin
58 from mercurial.node import bin
59
59
60 cmdtable = {}
60 cmdtable = {}
61 command = cmdutil.command(cmdtable)
61 command = cmdutil.command(cmdtable)
62 testedwith = 'internal'
62 testedwith = 'internal'
63
63
64 def prompt(ui, prompt, default=None, rest=':'):
64 def prompt(ui, prompt, default=None, rest=':'):
65 if default:
65 if default:
66 prompt += ' [%s]' % default
66 prompt += ' [%s]' % default
67 return ui.prompt(prompt + rest, default)
67 return ui.prompt(prompt + rest, default)
68
68
69 def introwanted(opts, number):
69 def introwanted(opts, number):
70 '''is an introductory message apparently wanted?'''
70 '''is an introductory message apparently wanted?'''
71 return number > 1 or opts.get('intro') or opts.get('desc')
71 return number > 1 or opts.get('intro') or opts.get('desc')
72
72
73 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total, numbered,
73 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total, numbered,
74 patchname=None):
74 patchname=None):
75
75
76 desc = []
76 desc = []
77 node = None
77 node = None
78 body = ''
78 body = ''
79
79
80 for line in patchlines:
80 for line in patchlines:
81 if line.startswith('#'):
81 if line.startswith('#'):
82 if line.startswith('# Node ID'):
82 if line.startswith('# Node ID'):
83 node = line.split()[-1]
83 node = line.split()[-1]
84 continue
84 continue
85 if line.startswith('diff -r') or line.startswith('diff --git'):
85 if line.startswith('diff -r') or line.startswith('diff --git'):
86 break
86 break
87 desc.append(line)
87 desc.append(line)
88
88
89 if not patchname and not node:
89 if not patchname and not node:
90 raise ValueError
90 raise ValueError
91
91
92 if opts.get('attach') and not opts.get('body'):
92 if opts.get('attach') and not opts.get('body'):
93 body = ('\n'.join(desc[1:]).strip() or
93 body = ('\n'.join(desc[1:]).strip() or
94 'Patch subject is complete summary.')
94 'Patch subject is complete summary.')
95 body += '\n\n\n'
95 body += '\n\n\n'
96
96
97 if opts.get('plain'):
97 if opts.get('plain'):
98 while patchlines and patchlines[0].startswith('# '):
98 while patchlines and patchlines[0].startswith('# '):
99 patchlines.pop(0)
99 patchlines.pop(0)
100 if patchlines:
100 if patchlines:
101 patchlines.pop(0)
101 patchlines.pop(0)
102 while patchlines and not patchlines[0].strip():
102 while patchlines and not patchlines[0].strip():
103 patchlines.pop(0)
103 patchlines.pop(0)
104
104
105 ds = patch.diffstat(patchlines, git=opts.get('git'))
105 ds = patch.diffstat(patchlines, git=opts.get('git'))
106 if opts.get('diffstat'):
106 if opts.get('diffstat'):
107 body += ds + '\n\n'
107 body += ds + '\n\n'
108
108
109 addattachment = opts.get('attach') or opts.get('inline')
109 addattachment = opts.get('attach') or opts.get('inline')
110 if not addattachment or opts.get('body'):
110 if not addattachment or opts.get('body'):
111 body += '\n'.join(patchlines)
111 body += '\n'.join(patchlines)
112
112
113 if addattachment:
113 if addattachment:
114 msg = email.MIMEMultipart.MIMEMultipart()
114 msg = email.MIMEMultipart.MIMEMultipart()
115 if body:
115 if body:
116 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
116 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
117 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch',
117 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch',
118 opts.get('test'))
118 opts.get('test'))
119 binnode = bin(node)
119 binnode = bin(node)
120 # if node is mq patch, it will have the patch file's name as a tag
120 # if node is mq patch, it will have the patch file's name as a tag
121 if not patchname:
121 if not patchname:
122 patchtags = [t for t in repo.nodetags(binnode)
122 patchtags = [t for t in repo.nodetags(binnode)
123 if t.endswith('.patch') or t.endswith('.diff')]
123 if t.endswith('.patch') or t.endswith('.diff')]
124 if patchtags:
124 if patchtags:
125 patchname = patchtags[0]
125 patchname = patchtags[0]
126 elif total > 1:
126 elif total > 1:
127 patchname = cmdutil.makefilename(repo, '%b-%n.patch',
127 patchname = cmdutil.makefilename(repo, '%b-%n.patch',
128 binnode, seqno=idx,
128 binnode, seqno=idx,
129 total=total)
129 total=total)
130 else:
130 else:
131 patchname = cmdutil.makefilename(repo, '%b.patch', binnode)
131 patchname = cmdutil.makefilename(repo, '%b.patch', binnode)
132 disposition = 'inline'
132 disposition = 'inline'
133 if opts.get('attach'):
133 if opts.get('attach'):
134 disposition = 'attachment'
134 disposition = 'attachment'
135 p['Content-Disposition'] = disposition + '; filename=' + patchname
135 p['Content-Disposition'] = disposition + '; filename=' + patchname
136 msg.attach(p)
136 msg.attach(p)
137 else:
137 else:
138 msg = mail.mimetextpatch(body, display=opts.get('test'))
138 msg = mail.mimetextpatch(body, display=opts.get('test'))
139
139
140 flag = ' '.join(opts.get('flag'))
140 flag = ' '.join(opts.get('flag'))
141 if flag:
141 if flag:
142 flag = ' ' + flag
142 flag = ' ' + flag
143
143
144 subj = desc[0].strip().rstrip('. ')
144 subj = desc[0].strip().rstrip('. ')
145 if not numbered:
145 if not numbered:
146 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
146 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
147 else:
147 else:
148 tlen = len(str(total))
148 tlen = len(str(total))
149 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
149 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
150 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
150 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
151 msg['X-Mercurial-Node'] = node
151 msg['X-Mercurial-Node'] = node
152 msg['X-Mercurial-Series-Index'] = '%i' % idx
152 msg['X-Mercurial-Series-Index'] = '%i' % idx
153 msg['X-Mercurial-Series-Total'] = '%i' % total
153 msg['X-Mercurial-Series-Total'] = '%i' % total
154 return msg, subj, ds
154 return msg, subj, ds
155
155
156 emailopts = [
156 emailopts = [
157 ('', 'body', None, _('send patches as inline message text (default)')),
157 ('', 'body', None, _('send patches as inline message text (default)')),
158 ('a', 'attach', None, _('send patches as attachments')),
158 ('a', 'attach', None, _('send patches as attachments')),
159 ('i', 'inline', None, _('send patches as inline attachments')),
159 ('i', 'inline', None, _('send patches as inline attachments')),
160 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
160 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
161 ('c', 'cc', [], _('email addresses of copy recipients')),
161 ('c', 'cc', [], _('email addresses of copy recipients')),
162 ('', 'confirm', None, _('ask for confirmation before sending')),
162 ('', 'confirm', None, _('ask for confirmation before sending')),
163 ('d', 'diffstat', None, _('add diffstat output to messages')),
163 ('d', 'diffstat', None, _('add diffstat output to messages')),
164 ('', 'date', '', _('use the given date as the sending date')),
164 ('', 'date', '', _('use the given date as the sending date')),
165 ('', 'desc', '', _('use the given file as the series description')),
165 ('', 'desc', '', _('use the given file as the series description')),
166 ('f', 'from', '', _('email address of sender')),
166 ('f', 'from', '', _('email address of sender')),
167 ('n', 'test', None, _('print messages that would be sent')),
167 ('n', 'test', None, _('print messages that would be sent')),
168 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
168 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
169 ('', 'reply-to', [], _('email addresses replies should be sent to')),
169 ('', 'reply-to', [], _('email addresses replies should be sent to')),
170 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
170 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
171 ('', 'in-reply-to', '', _('message identifier to reply to')),
171 ('', 'in-reply-to', '', _('message identifier to reply to')),
172 ('', 'flag', [], _('flags to add in subject prefixes')),
172 ('', 'flag', [], _('flags to add in subject prefixes')),
173 ('t', 'to', [], _('email addresses of recipients'))]
173 ('t', 'to', [], _('email addresses of recipients'))]
174
174
175 @command('email',
175 @command('email',
176 [('g', 'git', None, _('use git extended diff format')),
176 [('g', 'git', None, _('use git extended diff format')),
177 ('', 'plain', None, _('omit hg patch header')),
177 ('', 'plain', None, _('omit hg patch header')),
178 ('o', 'outgoing', None,
178 ('o', 'outgoing', None,
179 _('send changes not found in the target repository')),
179 _('send changes not found in the target repository')),
180 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
180 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
181 ('', 'bundlename', 'bundle',
181 ('', 'bundlename', 'bundle',
182 _('name of the bundle attachment file'), _('NAME')),
182 _('name of the bundle attachment file'), _('NAME')),
183 ('r', 'rev', [], _('a revision to send'), _('REV')),
183 ('r', 'rev', [], _('a revision to send'), _('REV')),
184 ('', 'force', None, _('run even when remote repository is unrelated '
184 ('', 'force', None, _('run even when remote repository is unrelated '
185 '(with -b/--bundle)')),
185 '(with -b/--bundle)')),
186 ('', 'base', [], _('a base changeset to specify instead of a destination '
186 ('', 'base', [], _('a base changeset to specify instead of a destination '
187 '(with -b/--bundle)'), _('REV')),
187 '(with -b/--bundle)'), _('REV')),
188 ('', 'intro', None, _('send an introduction email for a single patch')),
188 ('', 'intro', None, _('send an introduction email for a single patch')),
189 ] + emailopts + commands.remoteopts,
189 ] + emailopts + commands.remoteopts,
190 _('hg email [OPTION]... [DEST]...'))
190 _('hg email [OPTION]... [DEST]...'))
191 def patchbomb(ui, repo, *revs, **opts):
191 def patchbomb(ui, repo, *revs, **opts):
192 '''send changesets by email
192 '''send changesets by email
193
193
194 By default, diffs are sent in the format generated by
194 By default, diffs are sent in the format generated by
195 :hg:`export`, one per message. The series starts with a "[PATCH 0
195 :hg:`export`, one per message. The series starts with a "[PATCH 0
196 of N]" introduction, which describes the series as a whole.
196 of N]" introduction, which describes the series as a whole.
197
197
198 Each patch email has a Subject line of "[PATCH M of N] ...", using
198 Each patch email has a Subject line of "[PATCH M of N] ...", using
199 the first line of the changeset description as the subject text.
199 the first line of the changeset description as the subject text.
200 The message contains two or three parts. First, the changeset
200 The message contains two or three parts. First, the changeset
201 description.
201 description.
202
202
203 With the -d/--diffstat option, if the diffstat program is
203 With the -d/--diffstat option, if the diffstat program is
204 installed, the result of running diffstat on the patch is inserted.
204 installed, the result of running diffstat on the patch is inserted.
205
205
206 Finally, the patch itself, as generated by :hg:`export`.
206 Finally, the patch itself, as generated by :hg:`export`.
207
207
208 With the -d/--diffstat or --confirm options, you will be presented
208 With the -d/--diffstat or --confirm options, you will be presented
209 with a final summary of all messages and asked for confirmation before
209 with a final summary of all messages and asked for confirmation before
210 the messages are sent.
210 the messages are sent.
211
211
212 By default the patch is included as text in the email body for
212 By default the patch is included as text in the email body for
213 easy reviewing. Using the -a/--attach option will instead create
213 easy reviewing. Using the -a/--attach option will instead create
214 an attachment for the patch. With -i/--inline an inline attachment
214 an attachment for the patch. With -i/--inline an inline attachment
215 will be created. You can include a patch both as text in the email
215 will be created. You can include a patch both as text in the email
216 body and as a regular or an inline attachment by combining the
216 body and as a regular or an inline attachment by combining the
217 -a/--attach or -i/--inline with the --body option.
217 -a/--attach or -i/--inline with the --body option.
218
218
219 With -o/--outgoing, emails will be generated for patches not found
219 With -o/--outgoing, emails will be generated for patches not found
220 in the destination repository (or only those which are ancestors
220 in the destination repository (or only those which are ancestors
221 of the specified revisions if any are provided)
221 of the specified revisions if any are provided)
222
222
223 With -b/--bundle, changesets are selected as for --outgoing, but a
223 With -b/--bundle, changesets are selected as for --outgoing, but a
224 single email containing a binary Mercurial bundle as an attachment
224 single email containing a binary Mercurial bundle as an attachment
225 will be sent.
225 will be sent.
226
226
227 With -m/--mbox, instead of previewing each patchbomb message in a
227 With -m/--mbox, instead of previewing each patchbomb message in a
228 pager or sending the messages directly, it will create a UNIX
228 pager or sending the messages directly, it will create a UNIX
229 mailbox file with the patch emails. This mailbox file can be
229 mailbox file with the patch emails. This mailbox file can be
230 previewed with any mail user agent which supports UNIX mbox
230 previewed with any mail user agent which supports UNIX mbox
231 files.
231 files.
232
232
233 With -n/--test, all steps will run, but mail will not be sent.
233 With -n/--test, all steps will run, but mail will not be sent.
234 You will be prompted for an email recipient address, a subject and
234 You will be prompted for an email recipient address, a subject and
235 an introductory message describing the patches of your patchbomb.
235 an introductory message describing the patches of your patchbomb.
236 Then when all is done, patchbomb messages are displayed. If the
236 Then when all is done, patchbomb messages are displayed. If the
237 PAGER environment variable is set, your pager will be fired up once
237 PAGER environment variable is set, your pager will be fired up once
238 for each patchbomb message, so you can verify everything is alright.
238 for each patchbomb message, so you can verify everything is alright.
239
239
240 In case email sending fails, you will find a backup of your series
240 In case email sending fails, you will find a backup of your series
241 introductory message in ``.hg/last-email.txt``.
241 introductory message in ``.hg/last-email.txt``.
242
242
243 Examples::
243 Examples::
244
244
245 hg email -r 3000 # send patch 3000 only
245 hg email -r 3000 # send patch 3000 only
246 hg email -r 3000 -r 3001 # send patches 3000 and 3001
246 hg email -r 3000 -r 3001 # send patches 3000 and 3001
247 hg email -r 3000:3005 # send patches 3000 through 3005
247 hg email -r 3000:3005 # send patches 3000 through 3005
248 hg email 3000 # send patch 3000 (deprecated)
248 hg email 3000 # send patch 3000 (deprecated)
249
249
250 hg email -o # send all patches not in default
250 hg email -o # send all patches not in default
251 hg email -o DEST # send all patches not in DEST
251 hg email -o DEST # send all patches not in DEST
252 hg email -o -r 3000 # send all ancestors of 3000 not in default
252 hg email -o -r 3000 # send all ancestors of 3000 not in default
253 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
253 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
254
254
255 hg email -b # send bundle of all patches not in default
255 hg email -b # send bundle of all patches not in default
256 hg email -b DEST # send bundle of all patches not in DEST
256 hg email -b DEST # send bundle of all patches not in DEST
257 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
257 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
258 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
258 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
259
259
260 hg email -o -m mbox && # generate an mbox file...
260 hg email -o -m mbox && # generate an mbox file...
261 mutt -R -f mbox # ... and view it with mutt
261 mutt -R -f mbox # ... and view it with mutt
262 hg email -o -m mbox && # generate an mbox file ...
262 hg email -o -m mbox && # generate an mbox file ...
263 formail -s sendmail \\ # ... and use formail to send from the mbox
263 formail -s sendmail \\ # ... and use formail to send from the mbox
264 -bm -t < mbox # ... using sendmail
264 -bm -t < mbox # ... using sendmail
265
265
266 Before using this command, you will need to enable email in your
266 Before using this command, you will need to enable email in your
267 hgrc. See the [email] section in hgrc(5) for details.
267 hgrc. See the [email] section in hgrc(5) for details.
268 '''
268 '''
269
269
270 _charsets = mail._charsets(ui)
270 _charsets = mail._charsets(ui)
271
271
272 bundle = opts.get('bundle')
272 bundle = opts.get('bundle')
273 date = opts.get('date')
273 date = opts.get('date')
274 mbox = opts.get('mbox')
274 mbox = opts.get('mbox')
275 outgoing = opts.get('outgoing')
275 outgoing = opts.get('outgoing')
276 rev = opts.get('rev')
276 rev = opts.get('rev')
277 # internal option used by pbranches
277 # internal option used by pbranches
278 patches = opts.get('patches')
278 patches = opts.get('patches')
279
279
280 def getoutgoing(dest, revs):
280 def getoutgoing(dest, revs):
281 '''Return the revisions present locally but not in dest'''
281 '''Return the revisions present locally but not in dest'''
282 url = ui.expandpath(dest or 'default-push', dest or 'default')
282 url = ui.expandpath(dest or 'default-push', dest or 'default')
283 url = hg.parseurl(url)[0]
283 url = hg.parseurl(url)[0]
284 ui.status(_('comparing with %s\n') % util.hidepassword(url))
284 ui.status(_('comparing with %s\n') % util.hidepassword(url))
285
285
286 revs = [r for r in scmutil.revrange(repo, revs) if r >= 0]
286 revs = [r for r in scmutil.revrange(repo, revs) if r >= 0]
287 if not revs:
287 if not revs:
288 revs = [len(repo) - 1]
288 revs = [len(repo) - 1]
289 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
289 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
290 if not revs:
290 if not revs:
291 ui.status(_("no changes found\n"))
291 ui.status(_("no changes found\n"))
292 return []
292 return []
293 return [str(r) for r in revs]
293 return [str(r) for r in revs]
294
294
295 def getpatches(revs):
295 def getpatches(revs):
296 prev = repo['.'].rev()
296 prev = repo['.'].rev()
297 for r in scmutil.revrange(repo, revs):
297 for r in scmutil.revrange(repo, revs):
298 if r == prev and (repo[None].files() or repo[None].deleted()):
298 if r == prev and (repo[None].files() or repo[None].deleted()):
299 ui.warn(_('warning: working directory has '
299 ui.warn(_('warning: working directory has '
300 'uncommitted changes\n'))
300 'uncommitted changes\n'))
301 output = cStringIO.StringIO()
301 output = cStringIO.StringIO()
302 cmdutil.export(repo, [r], fp=output,
302 cmdutil.export(repo, [r], fp=output,
303 opts=patch.diffopts(ui, opts))
303 opts=patch.diffopts(ui, opts))
304 yield output.getvalue().split('\n')
304 yield output.getvalue().split('\n')
305
305
306 def getbundle(dest):
306 def getbundle(dest):
307 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
307 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
308 tmpfn = os.path.join(tmpdir, 'bundle')
308 tmpfn = os.path.join(tmpdir, 'bundle')
309 try:
309 try:
310 commands.bundle(ui, repo, tmpfn, dest, **opts)
310 commands.bundle(ui, repo, tmpfn, dest, **opts)
311 fp = open(tmpfn, 'rb')
311 fp = open(tmpfn, 'rb')
312 data = fp.read()
312 data = fp.read()
313 fp.close()
313 fp.close()
314 return data
314 return data
315 finally:
315 finally:
316 try:
316 try:
317 os.unlink(tmpfn)
317 os.unlink(tmpfn)
318 except OSError:
318 except OSError:
319 pass
319 pass
320 os.rmdir(tmpdir)
320 os.rmdir(tmpdir)
321
321
322 if not (opts.get('test') or mbox):
322 if not (opts.get('test') or mbox):
323 # really sending
323 # really sending
324 mail.validateconfig(ui)
324 mail.validateconfig(ui)
325
325
326 if not (revs or rev or outgoing or bundle or patches):
326 if not (revs or rev or outgoing or bundle or patches):
327 raise util.Abort(_('specify at least one changeset with -r or -o'))
327 raise util.Abort(_('specify at least one changeset with -r or -o'))
328
328
329 if outgoing and bundle:
329 if outgoing and bundle:
330 raise util.Abort(_("--outgoing mode always on with --bundle;"
330 raise util.Abort(_("--outgoing mode always on with --bundle;"
331 " do not re-specify --outgoing"))
331 " do not re-specify --outgoing"))
332
332
333 if outgoing or bundle:
333 if outgoing or bundle:
334 if len(revs) > 1:
334 if len(revs) > 1:
335 raise util.Abort(_("too many destinations"))
335 raise util.Abort(_("too many destinations"))
336 dest = revs and revs[0] or None
336 dest = revs and revs[0] or None
337 revs = []
337 revs = []
338
338
339 if rev:
339 if rev:
340 if revs:
340 if revs:
341 raise util.Abort(_('use only one form to specify the revision'))
341 raise util.Abort(_('use only one form to specify the revision'))
342 revs = rev
342 revs = rev
343
343
344 if outgoing:
344 if outgoing:
345 revs = getoutgoing(dest, rev)
345 revs = getoutgoing(dest, rev)
346 if bundle:
346 if bundle:
347 opts['revs'] = revs
347 opts['revs'] = revs
348
348
349 # start
349 # start
350 if date:
350 if date:
351 start_time = util.parsedate(date)
351 start_time = util.parsedate(date)
352 else:
352 else:
353 start_time = util.makedate()
353 start_time = util.makedate()
354
354
355 def genmsgid(id):
355 def genmsgid(id):
356 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
356 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
357
357
358 def getdescription(body, sender):
358 def getdescription(body, sender):
359 if opts.get('desc'):
359 if opts.get('desc'):
360 body = open(opts.get('desc')).read()
360 body = open(opts.get('desc')).read()
361 else:
361 else:
362 ui.write(_('\nWrite the introductory message for the '
362 ui.write(_('\nWrite the introductory message for the '
363 'patch series.\n\n'))
363 'patch series.\n\n'))
364 body = ui.edit(body, sender)
364 body = ui.edit(body, sender)
365 # Save series description in case sendmail fails
365 # Save series description in case sendmail fails
366 msgfile = repo.opener('last-email.txt', 'wb')
366 msgfile = repo.opener('last-email.txt', 'wb')
367 msgfile.write(body)
367 msgfile.write(body)
368 msgfile.close()
368 msgfile.close()
369 return body
369 return body
370
370
371 def getpatchmsgs(patches, patchnames=None):
371 def getpatchmsgs(patches, patchnames=None):
372 msgs = []
372 msgs = []
373
373
374 ui.write(_('this patch series consists of %d patches.\n\n')
374 ui.write(_('this patch series consists of %d patches.\n\n')
375 % len(patches))
375 % len(patches))
376
376
377 # build the intro message, or skip it if the user declines
377 # build the intro message, or skip it if the user declines
378 if introwanted(opts, len(patches)):
378 if introwanted(opts, len(patches)):
379 msg = makeintro(patches)
379 msg = makeintro(patches)
380 if msg:
380 if msg:
381 msgs.append(msg)
381 msgs.append(msg)
382
382
383 # are we going to send more than one message?
383 # are we going to send more than one message?
384 numbered = len(msgs) + len(patches) > 1
384 numbered = len(msgs) + len(patches) > 1
385
385
386 # now generate the actual patch messages
386 # now generate the actual patch messages
387 name = None
387 name = None
388 for i, p in enumerate(patches):
388 for i, p in enumerate(patches):
389 if patchnames:
389 if patchnames:
390 name = patchnames[i]
390 name = patchnames[i]
391 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
391 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
392 len(patches), numbered, name)
392 len(patches), numbered, name)
393 msgs.append(msg)
393 msgs.append(msg)
394
394
395 return msgs
395 return msgs
396
396
397 def makeintro(patches):
397 def makeintro(patches):
398 tlen = len(str(len(patches)))
398 tlen = len(str(len(patches)))
399
399
400 flag = opts.get('flag') or ''
400 flag = opts.get('flag') or ''
401 if flag:
401 if flag:
402 flag = ' ' + ' '.join(flag)
402 flag = ' ' + ' '.join(flag)
403 prefix = '[PATCH %0*d of %d%s]' % (tlen, 0, len(patches), flag)
403 prefix = '[PATCH %0*d of %d%s]' % (tlen, 0, len(patches), flag)
404
404
405 subj = (opts.get('subject') or
405 subj = (opts.get('subject') or
406 prompt(ui, '(optional) Subject: ', rest=prefix, default=''))
406 prompt(ui, '(optional) Subject: ', rest=prefix, default=''))
407 if not subj:
407 if not subj:
408 return None # skip intro if the user doesn't bother
408 return None # skip intro if the user doesn't bother
409
409
410 subj = prefix + ' ' + subj
410 subj = prefix + ' ' + subj
411
411
412 body = ''
412 body = ''
413 if opts.get('diffstat'):
413 if opts.get('diffstat'):
414 # generate a cumulative diffstat of the whole patch series
414 # generate a cumulative diffstat of the whole patch series
415 diffstat = patch.diffstat(sum(patches, []))
415 diffstat = patch.diffstat(sum(patches, []))
416 body = '\n' + diffstat
416 body = '\n' + diffstat
417 else:
417 else:
418 diffstat = None
418 diffstat = None
419
419
420 body = getdescription(body, sender)
420 body = getdescription(body, sender)
421 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
421 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
422 msg['Subject'] = mail.headencode(ui, subj, _charsets,
422 msg['Subject'] = mail.headencode(ui, subj, _charsets,
423 opts.get('test'))
423 opts.get('test'))
424 return (msg, subj, diffstat)
424 return (msg, subj, diffstat)
425
425
426 def getbundlemsgs(bundle):
426 def getbundlemsgs(bundle):
427 subj = (opts.get('subject')
427 subj = (opts.get('subject')
428 or prompt(ui, 'Subject:', 'A bundle for your repository'))
428 or prompt(ui, 'Subject:', 'A bundle for your repository'))
429
429
430 body = getdescription('', sender)
430 body = getdescription('', sender)
431 msg = email.MIMEMultipart.MIMEMultipart()
431 msg = email.MIMEMultipart.MIMEMultipart()
432 if body:
432 if body:
433 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
433 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
434 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
434 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
435 datapart.set_payload(bundle)
435 datapart.set_payload(bundle)
436 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
436 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
437 datapart.add_header('Content-Disposition', 'attachment',
437 datapart.add_header('Content-Disposition', 'attachment',
438 filename=bundlename)
438 filename=bundlename)
439 email.Encoders.encode_base64(datapart)
439 email.Encoders.encode_base64(datapart)
440 msg.attach(datapart)
440 msg.attach(datapart)
441 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
441 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
442 return [(msg, subj, None)]
442 return [(msg, subj, None)]
443
443
444 sender = (opts.get('from') or ui.config('email', 'from') or
444 sender = (opts.get('from') or ui.config('email', 'from') or
445 ui.config('patchbomb', 'from') or
445 ui.config('patchbomb', 'from') or
446 prompt(ui, 'From', ui.username()))
446 prompt(ui, 'From', ui.username()))
447
447
448 if patches:
448 if patches:
449 msgs = getpatchmsgs(patches, opts.get('patchnames'))
449 msgs = getpatchmsgs(patches, opts.get('patchnames'))
450 elif bundle:
450 elif bundle:
451 msgs = getbundlemsgs(getbundle(dest))
451 msgs = getbundlemsgs(getbundle(dest))
452 else:
452 else:
453 msgs = getpatchmsgs(list(getpatches(revs)))
453 msgs = getpatchmsgs(list(getpatches(revs)))
454
454
455 showaddrs = []
455 showaddrs = []
456
456
457 def getaddrs(header, ask=False, default=None):
457 def getaddrs(header, ask=False, default=None):
458 configkey = header.lower()
458 configkey = header.lower()
459 opt = header.replace('-', '_').lower()
459 opt = header.replace('-', '_').lower()
460 addrs = opts.get(opt)
460 addrs = opts.get(opt)
461 if addrs:
461 if addrs:
462 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
462 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
463 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
463 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
464
464
465 # not on the command line: fallback to config and then maybe ask
465 # not on the command line: fallback to config and then maybe ask
466 addr = (ui.config('email', configkey) or
466 addr = (ui.config('email', configkey) or
467 ui.config('patchbomb', configkey) or
467 ui.config('patchbomb', configkey) or
468 '')
468 '')
469 if not addr and ask:
469 if not addr and ask:
470 addr = prompt(ui, header, default=default)
470 addr = prompt(ui, header, default=default)
471 if addr:
471 if addr:
472 showaddrs.append('%s: %s' % (header, addr))
472 showaddrs.append('%s: %s' % (header, addr))
473 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
473 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
474 else:
474 else:
475 return default
475 return default
476
476
477 to = getaddrs('To', ask=True)
477 to = getaddrs('To', ask=True)
478 if not to:
478 if not to:
479 # we can get here in non-interactive mode
479 # we can get here in non-interactive mode
480 raise util.Abort(_('no recipient addresses provided'))
480 raise util.Abort(_('no recipient addresses provided'))
481 cc = getaddrs('Cc', ask=True, default='') or []
481 cc = getaddrs('Cc', ask=True, default='') or []
482 bcc = getaddrs('Bcc') or []
482 bcc = getaddrs('Bcc') or []
483 replyto = getaddrs('Reply-To')
483 replyto = getaddrs('Reply-To')
484
484
485 if opts.get('diffstat') or opts.get('confirm'):
485 if opts.get('diffstat') or opts.get('confirm'):
486 ui.write(_('\nFinal summary:\n\n'))
486 ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
487 ui.write(('From: %s\n' % sender))
487 ui.write(('From: %s\n' % sender), label='patchbomb.from')
488 for addr in showaddrs:
488 for addr in showaddrs:
489 ui.write('%s\n' % addr)
489 ui.write('%s\n' % addr, label='patchbomb.to')
490 for m, subj, ds in msgs:
490 for m, subj, ds in msgs:
491 ui.write(('Subject: %s\n' % subj))
491 ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
492 if ds:
492 if ds:
493 ui.write(ds)
493 ui.write(ds, label='patchbomb.diffstats')
494 ui.write('\n')
494 ui.write('\n')
495 if ui.promptchoice(_('are you sure you want to send (yn)?'
495 if ui.promptchoice(_('are you sure you want to send (yn)?'
496 '$$ &Yes $$ &No')):
496 '$$ &Yes $$ &No')):
497 raise util.Abort(_('patchbomb canceled'))
497 raise util.Abort(_('patchbomb canceled'))
498
498
499 ui.write('\n')
499 ui.write('\n')
500
500
501 parent = opts.get('in_reply_to') or None
501 parent = opts.get('in_reply_to') or None
502 # angle brackets may be omitted, they're not semantically part of the msg-id
502 # angle brackets may be omitted, they're not semantically part of the msg-id
503 if parent is not None:
503 if parent is not None:
504 if not parent.startswith('<'):
504 if not parent.startswith('<'):
505 parent = '<' + parent
505 parent = '<' + parent
506 if not parent.endswith('>'):
506 if not parent.endswith('>'):
507 parent += '>'
507 parent += '>'
508
508
509 sender_addr = email.Utils.parseaddr(sender)[1]
509 sender_addr = email.Utils.parseaddr(sender)[1]
510 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
510 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
511 sendmail = None
511 sendmail = None
512 firstpatch = None
512 firstpatch = None
513 for i, (m, subj, ds) in enumerate(msgs):
513 for i, (m, subj, ds) in enumerate(msgs):
514 try:
514 try:
515 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
515 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
516 if not firstpatch:
516 if not firstpatch:
517 firstpatch = m['Message-Id']
517 firstpatch = m['Message-Id']
518 m['X-Mercurial-Series-Id'] = firstpatch
518 m['X-Mercurial-Series-Id'] = firstpatch
519 except TypeError:
519 except TypeError:
520 m['Message-Id'] = genmsgid('patchbomb')
520 m['Message-Id'] = genmsgid('patchbomb')
521 if parent:
521 if parent:
522 m['In-Reply-To'] = parent
522 m['In-Reply-To'] = parent
523 m['References'] = parent
523 m['References'] = parent
524 if not parent or 'X-Mercurial-Node' not in m:
524 if not parent or 'X-Mercurial-Node' not in m:
525 parent = m['Message-Id']
525 parent = m['Message-Id']
526
526
527 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
527 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
528 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
528 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
529
529
530 start_time = (start_time[0] + 1, start_time[1])
530 start_time = (start_time[0] + 1, start_time[1])
531 m['From'] = sender
531 m['From'] = sender
532 m['To'] = ', '.join(to)
532 m['To'] = ', '.join(to)
533 if cc:
533 if cc:
534 m['Cc'] = ', '.join(cc)
534 m['Cc'] = ', '.join(cc)
535 if bcc:
535 if bcc:
536 m['Bcc'] = ', '.join(bcc)
536 m['Bcc'] = ', '.join(bcc)
537 if replyto:
537 if replyto:
538 m['Reply-To'] = ', '.join(replyto)
538 m['Reply-To'] = ', '.join(replyto)
539 if opts.get('test'):
539 if opts.get('test'):
540 ui.status(_('displaying '), subj, ' ...\n')
540 ui.status(_('displaying '), subj, ' ...\n')
541 ui.flush()
541 ui.flush()
542 if 'PAGER' in os.environ and not ui.plain():
542 if 'PAGER' in os.environ and not ui.plain():
543 fp = util.popen(os.environ['PAGER'], 'w')
543 fp = util.popen(os.environ['PAGER'], 'w')
544 else:
544 else:
545 fp = ui
545 fp = ui
546 generator = email.Generator.Generator(fp, mangle_from_=False)
546 generator = email.Generator.Generator(fp, mangle_from_=False)
547 try:
547 try:
548 generator.flatten(m, 0)
548 generator.flatten(m, 0)
549 fp.write('\n')
549 fp.write('\n')
550 except IOError, inst:
550 except IOError, inst:
551 if inst.errno != errno.EPIPE:
551 if inst.errno != errno.EPIPE:
552 raise
552 raise
553 if fp is not ui:
553 if fp is not ui:
554 fp.close()
554 fp.close()
555 else:
555 else:
556 if not sendmail:
556 if not sendmail:
557 verifycert = ui.config('smtp', 'verifycert')
557 verifycert = ui.config('smtp', 'verifycert')
558 if opts.get('insecure'):
558 if opts.get('insecure'):
559 ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb')
559 ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb')
560 try:
560 try:
561 sendmail = mail.connect(ui, mbox=mbox)
561 sendmail = mail.connect(ui, mbox=mbox)
562 finally:
562 finally:
563 ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb')
563 ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb')
564 ui.status(_('sending '), subj, ' ...\n')
564 ui.status(_('sending '), subj, ' ...\n')
565 ui.progress(_('sending'), i, item=subj, total=len(msgs))
565 ui.progress(_('sending'), i, item=subj, total=len(msgs))
566 if not mbox:
566 if not mbox:
567 # Exim does not remove the Bcc field
567 # Exim does not remove the Bcc field
568 del m['Bcc']
568 del m['Bcc']
569 fp = cStringIO.StringIO()
569 fp = cStringIO.StringIO()
570 generator = email.Generator.Generator(fp, mangle_from_=False)
570 generator = email.Generator.Generator(fp, mangle_from_=False)
571 generator.flatten(m, 0)
571 generator.flatten(m, 0)
572 sendmail(sender_addr, to + bcc + cc, fp.getvalue())
572 sendmail(sender_addr, to + bcc + cc, fp.getvalue())
573
573
574 ui.progress(_('writing'), None)
574 ui.progress(_('writing'), None)
575 ui.progress(_('sending'), None)
575 ui.progress(_('sending'), None)
General Comments 0
You need to be logged in to leave comments. Login now