##// END OF EJS Templates
Add common bundle/outgoing options to hg email
John Goerzen -
r4279:126d1967 default
parent child Browse files
Show More
@@ -1,385 +1,391
1 # Command for sending a collection of Mercurial changesets as a series
1 # Command for sending a collection of Mercurial changesets as a series
2 # of patch emails.
2 # of patch emails.
3 #
3 #
4 # The series is started off with a "[PATCH 0 of N]" introduction,
4 # The series is started off with a "[PATCH 0 of N]" introduction,
5 # which describes the series as a whole.
5 # which describes the series as a whole.
6 #
6 #
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
8 # the first line of the changeset description as the subject text.
8 # the first line of the changeset description as the subject text.
9 # The message contains two or three body parts:
9 # The message contains two or three body parts:
10 #
10 #
11 # The remainder of the changeset description.
11 # The remainder of the changeset description.
12 #
12 #
13 # [Optional] If the diffstat program is installed, the result of
13 # [Optional] If the diffstat program is installed, the result of
14 # running diffstat on the patch.
14 # running diffstat on the patch.
15 #
15 #
16 # The patch itself, as generated by "hg export".
16 # The patch itself, as generated by "hg export".
17 #
17 #
18 # Each message refers to all of its predecessors using the In-Reply-To
18 # Each message refers to all of its predecessors using the In-Reply-To
19 # and References headers, so they will show up as a sequence in
19 # and References headers, so they will show up as a sequence in
20 # threaded mail and news readers, and in mail archives.
20 # threaded mail and news readers, and in mail archives.
21 #
21 #
22 # For each changeset, you will be prompted with a diffstat summary and
22 # For each changeset, you will be prompted with a diffstat summary and
23 # the changeset summary, so you can be sure you are sending the right
23 # the changeset summary, so you can be sure you are sending the right
24 # changes.
24 # changes.
25 #
25 #
26 # To enable this extension:
26 # To enable this extension:
27 #
27 #
28 # [extensions]
28 # [extensions]
29 # hgext.patchbomb =
29 # hgext.patchbomb =
30 #
30 #
31 # To configure other defaults, add a section like this to your hgrc
31 # To configure other defaults, add a section like this to your hgrc
32 # file:
32 # file:
33 #
33 #
34 # [email]
34 # [email]
35 # from = My Name <my@email>
35 # from = My Name <my@email>
36 # to = recipient1, recipient2, ...
36 # to = recipient1, recipient2, ...
37 # cc = cc1, cc2, ...
37 # cc = cc1, cc2, ...
38 # bcc = bcc1, bcc2, ...
38 # bcc = bcc1, bcc2, ...
39 #
39 #
40 # Then you can use the "hg email" command to mail a series of changesets
40 # Then you can use the "hg email" command to mail a series of changesets
41 # as a patchbomb.
41 # as a patchbomb.
42 #
42 #
43 # To avoid sending patches prematurely, it is a good idea to first run
43 # To avoid sending patches prematurely, it is a good idea to first run
44 # the "email" command with the "-n" option (test only). You will be
44 # the "email" command with the "-n" option (test only). You will be
45 # prompted for an email recipient address, a subject an an introductory
45 # prompted for an email recipient address, a subject an an introductory
46 # message describing the patches of your patchbomb. Then when all is
46 # message describing the patches of your patchbomb. Then when all is
47 # done, your pager will be fired up once for each patchbomb message, so
47 # done, your pager will be fired up once for each patchbomb message, so
48 # you can verify everything is alright.
48 # you can verify everything is alright.
49 #
49 #
50 # The "-m" (mbox) option is also very useful. Instead of previewing
50 # The "-m" (mbox) option is also very useful. Instead of previewing
51 # each patchbomb message in a pager or sending the messages directly,
51 # each patchbomb message in a pager or sending the messages directly,
52 # it will create a UNIX mailbox file with the patch emails. This
52 # it will create a UNIX mailbox file with the patch emails. This
53 # mailbox file can be previewed with any mail user agent which supports
53 # mailbox file can be previewed with any mail user agent which supports
54 # UNIX mbox files, i.e. with mutt:
54 # UNIX mbox files, i.e. with mutt:
55 #
55 #
56 # % mutt -R -f mbox
56 # % mutt -R -f mbox
57 #
57 #
58 # When you are previewing the patchbomb messages, you can use `formail'
58 # When you are previewing the patchbomb messages, you can use `formail'
59 # (a utility that is commonly installed as part of the procmail package),
59 # (a utility that is commonly installed as part of the procmail package),
60 # to send each message out:
60 # to send each message out:
61 #
61 #
62 # % formail -s sendmail -bm -t < mbox
62 # % formail -s sendmail -bm -t < mbox
63 #
63 #
64 # That should be all. Now your patchbomb is on its way out.
64 # That should be all. Now your patchbomb is on its way out.
65
65
66 import os, errno, socket, tempfile
66 import os, errno, socket, tempfile
67 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
67 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
68 import email.Utils, email.Encoders
68 import email.Utils, email.Encoders
69 from mercurial import cmdutil, commands, hg, mail, ui, patch, util
69 from mercurial import cmdutil, commands, hg, mail, ui, patch, util
70 from mercurial.i18n import _
70 from mercurial.i18n import _
71 from mercurial.node import *
71 from mercurial.node import *
72
72
73 try:
73 try:
74 # readline gives raw_input editing capabilities, but is not
74 # readline gives raw_input editing capabilities, but is not
75 # present on windows
75 # present on windows
76 import readline
76 import readline
77 except ImportError: pass
77 except ImportError: pass
78
78
79 def patchbomb(ui, repo, *revs, **opts):
79 def patchbomb(ui, repo, *revs, **opts):
80 '''send changesets as a series of patch emails
80 '''send changesets as a series of patch emails
81
81
82 The series starts with a "[PATCH 0 of N]" introduction, which
82 The series starts with a "[PATCH 0 of N]" introduction, which
83 describes the series as a whole.
83 describes the series as a whole.
84
84
85 Each patch email has a Subject line of "[PATCH M of N] ...", using
85 Each patch email has a Subject line of "[PATCH M of N] ...", using
86 the first line of the changeset description as the subject text.
86 the first line of the changeset description as the subject text.
87 The message contains two or three body parts. First, the rest of
87 The message contains two or three body parts. First, the rest of
88 the changeset description. Next, (optionally) if the diffstat
88 the changeset description. Next, (optionally) if the diffstat
89 program is installed, the result of running diffstat on the patch.
89 program is installed, the result of running diffstat on the patch.
90 Finally, the patch itself, as generated by "hg export".
90 Finally, the patch itself, as generated by "hg export".
91
91
92 With --outgoing, emails will be generated for patches not
92 With --outgoing, emails will be generated for patches not
93 found in the target repository (or only those which are
93 found in the target repository (or only those which are
94 ancestors of the specified revisions if any are provided)
94 ancestors of the specified revisions if any are provided)
95 '''
95 '''
96
96
97 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
97 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
98 if default: prompt += ' [%s]' % default
98 if default: prompt += ' [%s]' % default
99 prompt += rest
99 prompt += rest
100 while True:
100 while True:
101 r = raw_input(prompt)
101 r = raw_input(prompt)
102 if r: return r
102 if r: return r
103 if default is not None: return default
103 if default is not None: return default
104 if empty_ok: return r
104 if empty_ok: return r
105 ui.warn(_('Please enter a valid value.\n'))
105 ui.warn(_('Please enter a valid value.\n'))
106
106
107 def confirm(s):
107 def confirm(s):
108 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
108 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
109 raise ValueError
109 raise ValueError
110
110
111 def cdiffstat(summary, patchlines):
111 def cdiffstat(summary, patchlines):
112 s = patch.diffstat(patchlines)
112 s = patch.diffstat(patchlines)
113 if s:
113 if s:
114 if summary:
114 if summary:
115 ui.write(summary, '\n')
115 ui.write(summary, '\n')
116 ui.write(s, '\n')
116 ui.write(s, '\n')
117 confirm(_('Does the diffstat above look okay'))
117 confirm(_('Does the diffstat above look okay'))
118 return s
118 return s
119
119
120 def makepatch(patch, idx, total):
120 def makepatch(patch, idx, total):
121 desc = []
121 desc = []
122 node = None
122 node = None
123 body = ''
123 body = ''
124 for line in patch:
124 for line in patch:
125 if line.startswith('#'):
125 if line.startswith('#'):
126 if line.startswith('# Node ID'): node = line.split()[-1]
126 if line.startswith('# Node ID'): node = line.split()[-1]
127 continue
127 continue
128 if (line.startswith('diff -r')
128 if (line.startswith('diff -r')
129 or line.startswith('diff --git')):
129 or line.startswith('diff --git')):
130 break
130 break
131 desc.append(line)
131 desc.append(line)
132 if not node: raise ValueError
132 if not node: raise ValueError
133
133
134 #body = ('\n'.join(desc[1:]).strip() or
134 #body = ('\n'.join(desc[1:]).strip() or
135 # 'Patch subject is complete summary.')
135 # 'Patch subject is complete summary.')
136 #body += '\n\n\n'
136 #body += '\n\n\n'
137
137
138 if opts['plain']:
138 if opts['plain']:
139 while patch and patch[0].startswith('# '): patch.pop(0)
139 while patch and patch[0].startswith('# '): patch.pop(0)
140 if patch: patch.pop(0)
140 if patch: patch.pop(0)
141 while patch and not patch[0].strip(): patch.pop(0)
141 while patch and not patch[0].strip(): patch.pop(0)
142 if opts['diffstat']:
142 if opts['diffstat']:
143 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
143 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
144 if opts['attach']:
144 if opts['attach']:
145 msg = email.MIMEMultipart.MIMEMultipart()
145 msg = email.MIMEMultipart.MIMEMultipart()
146 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
146 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
147 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
147 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
148 binnode = bin(node)
148 binnode = bin(node)
149 # if node is mq patch, it will have patch file name as tag
149 # if node is mq patch, it will have patch file name as tag
150 patchname = [t for t in repo.nodetags(binnode)
150 patchname = [t for t in repo.nodetags(binnode)
151 if t.endswith('.patch') or t.endswith('.diff')]
151 if t.endswith('.patch') or t.endswith('.diff')]
152 if patchname:
152 if patchname:
153 patchname = patchname[0]
153 patchname = patchname[0]
154 elif total > 1:
154 elif total > 1:
155 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
155 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
156 binnode, idx, total)
156 binnode, idx, total)
157 else:
157 else:
158 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
158 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
159 p['Content-Disposition'] = 'inline; filename=' + patchname
159 p['Content-Disposition'] = 'inline; filename=' + patchname
160 msg.attach(p)
160 msg.attach(p)
161 else:
161 else:
162 body += '\n'.join(patch)
162 body += '\n'.join(patch)
163 msg = email.MIMEText.MIMEText(body)
163 msg = email.MIMEText.MIMEText(body)
164
164
165 subj = desc[0].strip().rstrip('. ')
165 subj = desc[0].strip().rstrip('. ')
166 if total == 1:
166 if total == 1:
167 subj = '[PATCH] ' + (opts['subject'] or subj)
167 subj = '[PATCH] ' + (opts['subject'] or subj)
168 else:
168 else:
169 tlen = len(str(total))
169 tlen = len(str(total))
170 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
170 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
171 msg['Subject'] = subj
171 msg['Subject'] = subj
172 msg['X-Mercurial-Node'] = node
172 msg['X-Mercurial-Node'] = node
173 return msg
173 return msg
174
174
175 def outgoing(dest, revs):
175 def outgoing(dest, revs):
176 '''Return the revisions present locally but not in dest'''
176 '''Return the revisions present locally but not in dest'''
177 dest = ui.expandpath(dest or 'default-push', dest or 'default')
177 dest = ui.expandpath(dest or 'default-push', dest or 'default')
178 revs = [repo.lookup(rev) for rev in revs]
178 revs = [repo.lookup(rev) for rev in revs]
179 other = hg.repository(ui, dest)
179 other = hg.repository(ui, dest)
180 ui.status(_('comparing with %s\n') % dest)
180 ui.status(_('comparing with %s\n') % dest)
181 o = repo.findoutgoing(other)
181 o = repo.findoutgoing(other)
182 if not o:
182 if not o:
183 ui.status(_("no changes found\n"))
183 ui.status(_("no changes found\n"))
184 return []
184 return []
185 o = repo.changelog.nodesbetween(o, revs or None)[0]
185 o = repo.changelog.nodesbetween(o, revs or None)[0]
186 return [str(repo.changelog.rev(r)) for r in o]
186 return [str(repo.changelog.rev(r)) for r in o]
187
187
188 def getbundle(dest, revs):
188 def getbundle(dest):
189 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
189 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
190 tmpfn = os.path.join(tmpdir, 'bundle')
190 tmpfn = os.path.join(tmpdir, 'bundle')
191 try:
191 try:
192 commands.bundle(ui, repo, tmpfn, dest, *revs, **{'force': 0})
192 commands.bundle(ui, repo, tmpfn, dest, **opts)
193 return open(tmpfn).read()
193 return open(tmpfn).read()
194 finally:
194 finally:
195 try:
195 try:
196 os.unlink(tmpfn)
196 os.unlink(tmpfn)
197 except:
197 except:
198 pass
198 pass
199 os.rmdir(tmpdir)
199 os.rmdir(tmpdir)
200
200
201 # option handling
201 # option handling
202 commands.setremoteconfig(ui, opts)
202 commands.setremoteconfig(ui, opts)
203 if opts.get('outgoint') and opts.get('bundle'):
203 if opts.get('outgoint') and opts.get('bundle'):
204 raise util.Abort(_("--outgoing mode always on with --bundle; do not re-specify --outgoing"))
204 raise util.Abort(_("--outgoing mode always on with --bundle; do not re-specify --outgoing"))
205
205
206 if opts.get('outgoing') or opts.get('bundle'):
206 if opts.get('outgoing') or opts.get('bundle'):
207 if len(revs) > 1:
207 if len(revs) > 1:
208 raise util.Abort(_("too many destinations"))
208 raise util.Abort(_("too many destinations"))
209 dest = revs and revs[0] or None
209 dest = revs and revs[0] or None
210 revs = []
210 revs = []
211
211
212 if opts.get('rev'):
212 if opts.get('rev'):
213 if revs:
213 if revs:
214 raise util.Abort(_('use only one form to specify the revision'))
214 raise util.Abort(_('use only one form to specify the revision'))
215 revs = opts.get('rev')
215 revs = opts.get('rev')
216
216
217 if opts.get('outgoing'):
217 if opts.get('outgoing'):
218 revs = outgoing(dest, opts.get('rev'))
218 revs = outgoing(dest, opts.get('rev'))
219 if opts.get('bundle'):
220 opts['revs'] = revs
219
221
220 # start
222 # start
221 start_time = util.makedate()
223 start_time = util.makedate()
222
224
223 def genmsgid(id):
225 def genmsgid(id):
224 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
226 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
225
227
226 sender = (opts['from'] or ui.config('email', 'from') or
228 sender = (opts['from'] or ui.config('email', 'from') or
227 ui.config('patchbomb', 'from') or
229 ui.config('patchbomb', 'from') or
228 prompt('From', ui.username()))
230 prompt('From', ui.username()))
229
231
230 def getaddrs(opt, prpt, default = None):
232 def getaddrs(opt, prpt, default = None):
231 addrs = opts[opt] or (ui.config('email', opt) or
233 addrs = opts[opt] or (ui.config('email', opt) or
232 ui.config('patchbomb', opt) or
234 ui.config('patchbomb', opt) or
233 prompt(prpt, default = default)).split(',')
235 prompt(prpt, default = default)).split(',')
234 return [a.strip() for a in addrs if a.strip()]
236 return [a.strip() for a in addrs if a.strip()]
235
237
236 to = getaddrs('to', 'To')
238 to = getaddrs('to', 'To')
237 cc = getaddrs('cc', 'Cc', '')
239 cc = getaddrs('cc', 'Cc', '')
238
240
239 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
241 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
240 ui.config('patchbomb', 'bcc') or '').split(',')
242 ui.config('patchbomb', 'bcc') or '').split(',')
241 bcc = [a.strip() for a in bcc if a.strip()]
243 bcc = [a.strip() for a in bcc if a.strip()]
242
244
243 def getexportmsgs():
245 def getexportmsgs():
244 patches = []
246 patches = []
245
247
246 class exportee:
248 class exportee:
247 def __init__(self, container):
249 def __init__(self, container):
248 self.lines = []
250 self.lines = []
249 self.container = container
251 self.container = container
250 self.name = 'email'
252 self.name = 'email'
251
253
252 def write(self, data):
254 def write(self, data):
253 self.lines.append(data)
255 self.lines.append(data)
254
256
255 def close(self):
257 def close(self):
256 self.container.append(''.join(self.lines).split('\n'))
258 self.container.append(''.join(self.lines).split('\n'))
257 self.lines = []
259 self.lines = []
258
260
259 commands.export(ui, repo, *revs, **{'output': exportee(patches),
261 commands.export(ui, repo, *revs, **{'output': exportee(patches),
260 'switch_parent': False,
262 'switch_parent': False,
261 'text': None,
263 'text': None,
262 'git': opts.get('git')})
264 'git': opts.get('git')})
263
265
264 jumbo = []
266 jumbo = []
265 msgs = []
267 msgs = []
266
268
267 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
269 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
268
270
269 for p, i in zip(patches, xrange(len(patches))):
271 for p, i in zip(patches, xrange(len(patches))):
270 jumbo.extend(p)
272 jumbo.extend(p)
271 msgs.append(makepatch(p, i + 1, len(patches)))
273 msgs.append(makepatch(p, i + 1, len(patches)))
272
274
273 if len(patches) > 1:
275 if len(patches) > 1:
274 tlen = len(str(len(patches)))
276 tlen = len(str(len(patches)))
275
277
276 subj = '[PATCH %0*d of %d] %s' % (
278 subj = '[PATCH %0*d of %d] %s' % (
277 tlen, 0,
279 tlen, 0,
278 len(patches),
280 len(patches),
279 opts['subject'] or
281 opts['subject'] or
280 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
282 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
281 len(patches))))
283 len(patches))))
282
284
283 body = ''
285 body = ''
284 if opts['diffstat']:
286 if opts['diffstat']:
285 d = cdiffstat(_('Final summary:\n'), jumbo)
287 d = cdiffstat(_('Final summary:\n'), jumbo)
286 if d: body = '\n' + d
288 if d: body = '\n' + d
287
289
288 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
290 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
289 body = ui.edit(body, sender)
291 body = ui.edit(body, sender)
290
292
291 msg = email.MIMEText.MIMEText(body)
293 msg = email.MIMEText.MIMEText(body)
292 msg['Subject'] = subj
294 msg['Subject'] = subj
293
295
294 msgs.insert(0, msg)
296 msgs.insert(0, msg)
295 return msgs
297 return msgs
296
298
297 def getbundlemsgs(bundle):
299 def getbundlemsgs(bundle):
298 subj = opts['subject'] or \
300 subj = opts['subject'] or \
299 prompt('Subject:', default='A bundle for your repository')
301 prompt('Subject:', default='A bundle for your repository')
300 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
302 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
301 body = ui.edit('', sender)
303 body = ui.edit('', sender)
302
304
303 msg = email.MIMEMultipart.MIMEMultipart()
305 msg = email.MIMEMultipart.MIMEMultipart()
304 if body:
306 if body:
305 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
307 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
306 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
308 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
307 datapart.set_payload(bundle)
309 datapart.set_payload(bundle)
308 email.Encoders.encode_base64(datapart)
310 email.Encoders.encode_base64(datapart)
309 msg.attach(datapart)
311 msg.attach(datapart)
310 msg['Subject'] = subj
312 msg['Subject'] = subj
311 return [msg]
313 return [msg]
312
314
313 if opts.get('bundle'):
315 if opts.get('bundle'):
314 msgs = getbundlemsgs(getbundle(dest, revs))
316 msgs = getbundlemsgs(getbundle(dest))
315 else:
317 else:
316 msgs = getexportmsgs()
318 msgs = getexportmsgs()
317
319
318 ui.write('\n')
320 ui.write('\n')
319
321
320 if not opts['test'] and not opts['mbox']:
322 if not opts['test'] and not opts['mbox']:
321 mailer = mail.connect(ui)
323 mailer = mail.connect(ui)
322 parent = None
324 parent = None
323
325
324 sender_addr = email.Utils.parseaddr(sender)[1]
326 sender_addr = email.Utils.parseaddr(sender)[1]
325 for m in msgs:
327 for m in msgs:
326 try:
328 try:
327 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
329 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
328 except TypeError:
330 except TypeError:
329 m['Message-Id'] = genmsgid('patchbomb')
331 m['Message-Id'] = genmsgid('patchbomb')
330 if parent:
332 if parent:
331 m['In-Reply-To'] = parent
333 m['In-Reply-To'] = parent
332 else:
334 else:
333 parent = m['Message-Id']
335 parent = m['Message-Id']
334 m['Date'] = util.datestr(date=start_time,
336 m['Date'] = util.datestr(date=start_time,
335 format="%a, %d %b %Y %H:%M:%S", timezone=True)
337 format="%a, %d %b %Y %H:%M:%S", timezone=True)
336
338
337 start_time = (start_time[0] + 1, start_time[1])
339 start_time = (start_time[0] + 1, start_time[1])
338 m['From'] = sender
340 m['From'] = sender
339 m['To'] = ', '.join(to)
341 m['To'] = ', '.join(to)
340 if cc: m['Cc'] = ', '.join(cc)
342 if cc: m['Cc'] = ', '.join(cc)
341 if bcc: m['Bcc'] = ', '.join(bcc)
343 if bcc: m['Bcc'] = ', '.join(bcc)
342 if opts['test']:
344 if opts['test']:
343 ui.status('Displaying ', m['Subject'], ' ...\n')
345 ui.status('Displaying ', m['Subject'], ' ...\n')
344 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
346 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
345 try:
347 try:
346 fp.write(m.as_string(0))
348 fp.write(m.as_string(0))
347 fp.write('\n')
349 fp.write('\n')
348 except IOError, inst:
350 except IOError, inst:
349 if inst.errno != errno.EPIPE:
351 if inst.errno != errno.EPIPE:
350 raise
352 raise
351 fp.close()
353 fp.close()
352 elif opts['mbox']:
354 elif opts['mbox']:
353 ui.status('Writing ', m['Subject'], ' ...\n')
355 ui.status('Writing ', m['Subject'], ' ...\n')
354 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
356 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
355 date = util.datestr(date=start_time,
357 date = util.datestr(date=start_time,
356 format='%a %b %d %H:%M:%S %Y', timezone=False)
358 format='%a %b %d %H:%M:%S %Y', timezone=False)
357 fp.write('From %s %s\n' % (sender_addr, date))
359 fp.write('From %s %s\n' % (sender_addr, date))
358 fp.write(m.as_string(0))
360 fp.write(m.as_string(0))
359 fp.write('\n\n')
361 fp.write('\n\n')
360 fp.close()
362 fp.close()
361 else:
363 else:
362 ui.status('Sending ', m['Subject'], ' ...\n')
364 ui.status('Sending ', m['Subject'], ' ...\n')
363 # Exim does not remove the Bcc field
365 # Exim does not remove the Bcc field
364 del m['Bcc']
366 del m['Bcc']
365 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
367 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
366
368
367 cmdtable = {
369 cmdtable = {
368 'email':
370 'email':
369 (patchbomb,
371 (patchbomb,
370 [('a', 'attach', None, 'send patches as inline attachments'),
372 [('a', 'attach', None, 'send patches as inline attachments'),
371 ('', 'bcc', [], 'email addresses of blind copy recipients'),
373 ('', 'bcc', [], 'email addresses of blind copy recipients'),
372 ('c', 'cc', [], 'email addresses of copy recipients'),
374 ('c', 'cc', [], 'email addresses of copy recipients'),
373 ('d', 'diffstat', None, 'add diffstat output to messages'),
375 ('d', 'diffstat', None, 'add diffstat output to messages'),
374 ('g', 'git', None, _('use git extended diff format')),
376 ('g', 'git', None, _('use git extended diff format')),
375 ('f', 'from', '', 'email address of sender'),
377 ('f', 'from', '', 'email address of sender'),
376 ('', 'plain', None, 'omit hg patch header'),
378 ('', 'plain', None, 'omit hg patch header'),
377 ('n', 'test', None, 'print messages that would be sent'),
379 ('n', 'test', None, 'print messages that would be sent'),
378 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
380 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
379 ('o', 'outgoing', None, _('send changes not found in the target repository')),
381 ('o', 'outgoing', None, _('send changes not found in the target repository')),
380 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
382 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
381 ('r', 'rev', [], _('a revision to send')),
383 ('r', 'rev', [], _('a revision to send')),
382 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
384 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
383 ('t', 'to', [], 'email addresses of recipients')] + commands.remoteopts,
385 ('t', 'to', [], 'email addresses of recipients'),
386 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
387 ('', 'base', [],
388 _('a base changeset to specify instead of a destination (with -b)'))]
389 + commands.remoteopts,
384 "hg email [OPTION]... [DEST]...")
390 "hg email [OPTION]... [DEST]...")
385 }
391 }
General Comments 0
You need to be logged in to leave comments. Login now