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