##// END OF EJS Templates
patchbomb: mime-encode clean utf-8 patches (issue814)...
Christian Ebert -
r7192:f31ba106 default
parent child Browse files
Show More
@@ -1,473 +1,474
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.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)
114 _charsets = mail._charsets(ui)
115
115
116 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
116 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
117 if not ui.interactive:
117 if not ui.interactive:
118 return default
118 return default
119 if default:
119 if default:
120 prompt += ' [%s]' % default
120 prompt += ' [%s]' % default
121 prompt += rest
121 prompt += rest
122 while True:
122 while True:
123 r = ui.prompt(prompt, default=default)
123 r = ui.prompt(prompt, default=default)
124 if r:
124 if r:
125 return r
125 return r
126 if default is not None:
126 if default is not None:
127 return default
127 return default
128 if empty_ok:
128 if empty_ok:
129 return r
129 return r
130 ui.warn(_('Please enter a valid value.\n'))
130 ui.warn(_('Please enter a valid value.\n'))
131
131
132 def confirm(s, denial):
132 def confirm(s, denial):
133 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
133 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
134 raise util.Abort(denial)
134 raise util.Abort(denial)
135
135
136 def cdiffstat(summary, patchlines):
136 def cdiffstat(summary, patchlines):
137 s = patch.diffstat(patchlines)
137 s = patch.diffstat(patchlines)
138 if s:
138 if s:
139 if summary:
139 if summary:
140 ui.write(summary, '\n')
140 ui.write(summary, '\n')
141 ui.write(s, '\n')
141 ui.write(s, '\n')
142 confirm(_('Does the diffstat above look okay'),
142 confirm(_('Does the diffstat above look okay'),
143 _('diffstat rejected'))
143 _('diffstat rejected'))
144 elif s is None:
144 elif s is None:
145 ui.warn(_('No diffstat information available.\n'))
145 ui.warn(_('No diffstat information available.\n'))
146 s = ''
146 s = ''
147 return s
147 return s
148
148
149 def makepatch(patch, idx, total):
149 def makepatch(patch, idx, total):
150 desc = []
150 desc = []
151 node = None
151 node = None
152 body = ''
152 body = ''
153 for line in patch:
153 for line in patch:
154 if line.startswith('#'):
154 if line.startswith('#'):
155 if line.startswith('# Node ID'):
155 if line.startswith('# Node ID'):
156 node = line.split()[-1]
156 node = line.split()[-1]
157 continue
157 continue
158 if line.startswith('diff -r') or line.startswith('diff --git'):
158 if line.startswith('diff -r') or line.startswith('diff --git'):
159 break
159 break
160 desc.append(line)
160 desc.append(line)
161 if not node:
161 if not node:
162 raise ValueError
162 raise ValueError
163
163
164 if opts.get('attach'):
164 if opts.get('attach'):
165 body = ('\n'.join(desc[1:]).strip() or
165 body = ('\n'.join(desc[1:]).strip() or
166 'Patch subject is complete summary.')
166 'Patch subject is complete summary.')
167 body += '\n\n\n'
167 body += '\n\n\n'
168
168
169 if opts.get('plain'):
169 if opts.get('plain'):
170 while patch and patch[0].startswith('# '):
170 while patch and patch[0].startswith('# '):
171 patch.pop(0)
171 patch.pop(0)
172 if patch:
172 if patch:
173 patch.pop(0)
173 patch.pop(0)
174 while patch and not patch[0].strip():
174 while patch and not patch[0].strip():
175 patch.pop(0)
175 patch.pop(0)
176 if opts.get('diffstat'):
176 if opts.get('diffstat'):
177 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
177 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
178 if opts.get('attach') or opts.get('inline'):
178 if opts.get('attach') or opts.get('inline'):
179 msg = email.MIMEMultipart.MIMEMultipart()
179 msg = email.MIMEMultipart.MIMEMultipart()
180 if body:
180 if body:
181 msg.attach(mail.mimeencode(ui, body, _charsets,
181 msg.attach(mail.mimeencode(ui, body, _charsets,
182 opts.get('test')))
182 opts.get('test')))
183 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
183 p = mail.mimetextpatch('\n'.join(patch), 'x-patch',
184 opts.get('test'))
184 binnode = bin(node)
185 binnode = bin(node)
185 # if node is mq patch, it will have patch file name as tag
186 # if node is mq patch, it will have patch file name as tag
186 patchname = [t for t in repo.nodetags(binnode)
187 patchname = [t for t in repo.nodetags(binnode)
187 if t.endswith('.patch') or t.endswith('.diff')]
188 if t.endswith('.patch') or t.endswith('.diff')]
188 if patchname:
189 if patchname:
189 patchname = patchname[0]
190 patchname = patchname[0]
190 elif total > 1:
191 elif total > 1:
191 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
192 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
192 binnode, idx, total)
193 binnode, idx, total)
193 else:
194 else:
194 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
195 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
195 disposition = 'inline'
196 disposition = 'inline'
196 if opts.get('attach'):
197 if opts.get('attach'):
197 disposition = 'attachment'
198 disposition = 'attachment'
198 p['Content-Disposition'] = disposition + '; filename=' + patchname
199 p['Content-Disposition'] = disposition + '; filename=' + patchname
199 msg.attach(p)
200 msg.attach(p)
200 else:
201 else:
201 body += '\n'.join(patch)
202 body += '\n'.join(patch)
202 msg = email.MIMEText.MIMEText(body)
203 msg = mail.mimetextpatch(body, display=opts.get('test'))
203
204
204 subj = desc[0].strip().rstrip('. ')
205 subj = desc[0].strip().rstrip('. ')
205 if total == 1:
206 if total == 1:
206 subj = '[PATCH] ' + (opts.get('subject') or subj)
207 subj = '[PATCH] ' + (opts.get('subject') or subj)
207 else:
208 else:
208 tlen = len(str(total))
209 tlen = len(str(total))
209 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
210 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
210 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
211 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
211 msg['X-Mercurial-Node'] = node
212 msg['X-Mercurial-Node'] = node
212 return msg, subj
213 return msg, subj
213
214
214 def outgoing(dest, revs):
215 def outgoing(dest, revs):
215 '''Return the revisions present locally but not in dest'''
216 '''Return the revisions present locally but not in dest'''
216 dest = ui.expandpath(dest or 'default-push', dest or 'default')
217 dest = ui.expandpath(dest or 'default-push', dest or 'default')
217 revs = [repo.lookup(rev) for rev in revs]
218 revs = [repo.lookup(rev) for rev in revs]
218 other = hg.repository(ui, dest)
219 other = hg.repository(ui, dest)
219 ui.status(_('comparing with %s\n') % dest)
220 ui.status(_('comparing with %s\n') % dest)
220 o = repo.findoutgoing(other)
221 o = repo.findoutgoing(other)
221 if not o:
222 if not o:
222 ui.status(_("no changes found\n"))
223 ui.status(_("no changes found\n"))
223 return []
224 return []
224 o = repo.changelog.nodesbetween(o, revs or None)[0]
225 o = repo.changelog.nodesbetween(o, revs or None)[0]
225 return [str(repo.changelog.rev(r)) for r in o]
226 return [str(repo.changelog.rev(r)) for r in o]
226
227
227 def getbundle(dest):
228 def getbundle(dest):
228 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
229 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
229 tmpfn = os.path.join(tmpdir, 'bundle')
230 tmpfn = os.path.join(tmpdir, 'bundle')
230 try:
231 try:
231 commands.bundle(ui, repo, tmpfn, dest, **opts)
232 commands.bundle(ui, repo, tmpfn, dest, **opts)
232 return open(tmpfn, 'rb').read()
233 return open(tmpfn, 'rb').read()
233 finally:
234 finally:
234 try:
235 try:
235 os.unlink(tmpfn)
236 os.unlink(tmpfn)
236 except:
237 except:
237 pass
238 pass
238 os.rmdir(tmpdir)
239 os.rmdir(tmpdir)
239
240
240 if not (opts.get('test') or opts.get('mbox')):
241 if not (opts.get('test') or opts.get('mbox')):
241 # really sending
242 # really sending
242 mail.validateconfig(ui)
243 mail.validateconfig(ui)
243
244
244 if not (revs or opts.get('rev')
245 if not (revs or opts.get('rev')
245 or opts.get('outgoing') or opts.get('bundle')):
246 or opts.get('outgoing') or opts.get('bundle')):
246 raise util.Abort(_('specify at least one changeset with -r or -o'))
247 raise util.Abort(_('specify at least one changeset with -r or -o'))
247
248
248 cmdutil.setremoteconfig(ui, opts)
249 cmdutil.setremoteconfig(ui, opts)
249 if opts.get('outgoing') and opts.get('bundle'):
250 if opts.get('outgoing') and opts.get('bundle'):
250 raise util.Abort(_("--outgoing mode always on with --bundle;"
251 raise util.Abort(_("--outgoing mode always on with --bundle;"
251 " do not re-specify --outgoing"))
252 " do not re-specify --outgoing"))
252
253
253 if opts.get('outgoing') or opts.get('bundle'):
254 if opts.get('outgoing') or opts.get('bundle'):
254 if len(revs) > 1:
255 if len(revs) > 1:
255 raise util.Abort(_("too many destinations"))
256 raise util.Abort(_("too many destinations"))
256 dest = revs and revs[0] or None
257 dest = revs and revs[0] or None
257 revs = []
258 revs = []
258
259
259 if opts.get('rev'):
260 if opts.get('rev'):
260 if revs:
261 if revs:
261 raise util.Abort(_('use only one form to specify the revision'))
262 raise util.Abort(_('use only one form to specify the revision'))
262 revs = opts.get('rev')
263 revs = opts.get('rev')
263
264
264 if opts.get('outgoing'):
265 if opts.get('outgoing'):
265 revs = outgoing(dest, opts.get('rev'))
266 revs = outgoing(dest, opts.get('rev'))
266 if opts.get('bundle'):
267 if opts.get('bundle'):
267 opts['revs'] = revs
268 opts['revs'] = revs
268
269
269 # start
270 # start
270 if opts.get('date'):
271 if opts.get('date'):
271 start_time = util.parsedate(opts.get('date'))
272 start_time = util.parsedate(opts.get('date'))
272 else:
273 else:
273 start_time = util.makedate()
274 start_time = util.makedate()
274
275
275 def genmsgid(id):
276 def genmsgid(id):
276 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
277 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
277
278
278 def getdescription(body, sender):
279 def getdescription(body, sender):
279 if opts.get('desc'):
280 if opts.get('desc'):
280 body = open(opts.get('desc')).read()
281 body = open(opts.get('desc')).read()
281 else:
282 else:
282 ui.write(_('\nWrite the introductory message for the '
283 ui.write(_('\nWrite the introductory message for the '
283 'patch series.\n\n'))
284 'patch series.\n\n'))
284 body = ui.edit(body, sender)
285 body = ui.edit(body, sender)
285 return body
286 return body
286
287
287 def getexportmsgs():
288 def getexportmsgs():
288 patches = []
289 patches = []
289
290
290 class exportee:
291 class exportee:
291 def __init__(self, container):
292 def __init__(self, container):
292 self.lines = []
293 self.lines = []
293 self.container = container
294 self.container = container
294 self.name = 'email'
295 self.name = 'email'
295
296
296 def write(self, data):
297 def write(self, data):
297 self.lines.append(data)
298 self.lines.append(data)
298
299
299 def close(self):
300 def close(self):
300 self.container.append(''.join(self.lines).split('\n'))
301 self.container.append(''.join(self.lines).split('\n'))
301 self.lines = []
302 self.lines = []
302
303
303 commands.export(ui, repo, *revs, **{'output': exportee(patches),
304 commands.export(ui, repo, *revs, **{'output': exportee(patches),
304 'switch_parent': False,
305 'switch_parent': False,
305 'text': None,
306 'text': None,
306 'git': opts.get('git')})
307 'git': opts.get('git')})
307
308
308 jumbo = []
309 jumbo = []
309 msgs = []
310 msgs = []
310
311
311 ui.write(_('This patch series consists of %d patches.\n\n')
312 ui.write(_('This patch series consists of %d patches.\n\n')
312 % len(patches))
313 % len(patches))
313
314
314 for p, i in zip(patches, xrange(len(patches))):
315 for p, i in zip(patches, xrange(len(patches))):
315 jumbo.extend(p)
316 jumbo.extend(p)
316 msgs.append(makepatch(p, i + 1, len(patches)))
317 msgs.append(makepatch(p, i + 1, len(patches)))
317
318
318 if len(patches) > 1:
319 if len(patches) > 1:
319 tlen = len(str(len(patches)))
320 tlen = len(str(len(patches)))
320
321
321 subj = '[PATCH %0*d of %d] %s' % (
322 subj = '[PATCH %0*d of %d] %s' % (
322 tlen, 0, len(patches),
323 tlen, 0, len(patches),
323 opts.get('subject') or
324 opts.get('subject') or
324 prompt('Subject:',
325 prompt('Subject:',
325 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
326 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
326
327
327 body = ''
328 body = ''
328 if opts.get('diffstat'):
329 if opts.get('diffstat'):
329 d = cdiffstat(_('Final summary:\n'), jumbo)
330 d = cdiffstat(_('Final summary:\n'), jumbo)
330 if d:
331 if d:
331 body = '\n' + d
332 body = '\n' + d
332
333
333 body = getdescription(body, sender)
334 body = getdescription(body, sender)
334 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
335 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
335 msg['Subject'] = mail.headencode(ui, subj, _charsets,
336 msg['Subject'] = mail.headencode(ui, subj, _charsets,
336 opts.get('test'))
337 opts.get('test'))
337
338
338 msgs.insert(0, (msg, subj))
339 msgs.insert(0, (msg, subj))
339 return msgs
340 return msgs
340
341
341 def getbundlemsgs(bundle):
342 def getbundlemsgs(bundle):
342 subj = (opts.get('subject')
343 subj = (opts.get('subject')
343 or prompt('Subject:', default='A bundle for your repository'))
344 or prompt('Subject:', default='A bundle for your repository'))
344
345
345 body = getdescription('', sender)
346 body = getdescription('', sender)
346 msg = email.MIMEMultipart.MIMEMultipart()
347 msg = email.MIMEMultipart.MIMEMultipart()
347 if body:
348 if body:
348 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
349 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
349 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
350 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
350 datapart.set_payload(bundle)
351 datapart.set_payload(bundle)
351 datapart.add_header('Content-Disposition', 'attachment',
352 datapart.add_header('Content-Disposition', 'attachment',
352 filename='bundle.hg')
353 filename='bundle.hg')
353 email.Encoders.encode_base64(datapart)
354 email.Encoders.encode_base64(datapart)
354 msg.attach(datapart)
355 msg.attach(datapart)
355 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
356 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
356 return [(msg, subj)]
357 return [(msg, subj)]
357
358
358 sender = (opts.get('from') or ui.config('email', 'from') or
359 sender = (opts.get('from') or ui.config('email', 'from') or
359 ui.config('patchbomb', 'from') or
360 ui.config('patchbomb', 'from') or
360 prompt('From', ui.username()))
361 prompt('From', ui.username()))
361
362
362 if opts.get('bundle'):
363 if opts.get('bundle'):
363 msgs = getbundlemsgs(getbundle(dest))
364 msgs = getbundlemsgs(getbundle(dest))
364 else:
365 else:
365 msgs = getexportmsgs()
366 msgs = getexportmsgs()
366
367
367 def getaddrs(opt, prpt, default = None):
368 def getaddrs(opt, prpt, default = None):
368 addrs = opts.get(opt) or (ui.config('email', opt) or
369 addrs = opts.get(opt) or (ui.config('email', opt) or
369 ui.config('patchbomb', opt) or
370 ui.config('patchbomb', opt) or
370 prompt(prpt, default = default)).split(',')
371 prompt(prpt, default = default)).split(',')
371 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
372 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
372 for a in addrs if a.strip()]
373 for a in addrs if a.strip()]
373
374
374 to = getaddrs('to', 'To')
375 to = getaddrs('to', 'To')
375 cc = getaddrs('cc', 'Cc', '')
376 cc = getaddrs('cc', 'Cc', '')
376
377
377 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
378 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
378 ui.config('patchbomb', 'bcc') or '').split(',')
379 ui.config('patchbomb', 'bcc') or '').split(',')
379 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
380 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
380 for a in bcc if a.strip()]
381 for a in bcc if a.strip()]
381
382
382 ui.write('\n')
383 ui.write('\n')
383
384
384 parent = None
385 parent = None
385
386
386 sender_addr = email.Utils.parseaddr(sender)[1]
387 sender_addr = email.Utils.parseaddr(sender)[1]
387 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
388 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
388 sendmail = None
389 sendmail = None
389 for m, subj in msgs:
390 for m, subj in msgs:
390 try:
391 try:
391 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
392 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
392 except TypeError:
393 except TypeError:
393 m['Message-Id'] = genmsgid('patchbomb')
394 m['Message-Id'] = genmsgid('patchbomb')
394 if parent:
395 if parent:
395 m['In-Reply-To'] = parent
396 m['In-Reply-To'] = parent
396 else:
397 else:
397 parent = m['Message-Id']
398 parent = m['Message-Id']
398 m['Date'] = util.datestr(start_time, "%a, %d %b %Y %H:%M:%S %1%2")
399 m['Date'] = util.datestr(start_time, "%a, %d %b %Y %H:%M:%S %1%2")
399
400
400 start_time = (start_time[0] + 1, start_time[1])
401 start_time = (start_time[0] + 1, start_time[1])
401 m['From'] = sender
402 m['From'] = sender
402 m['To'] = ', '.join(to)
403 m['To'] = ', '.join(to)
403 if cc:
404 if cc:
404 m['Cc'] = ', '.join(cc)
405 m['Cc'] = ', '.join(cc)
405 if bcc:
406 if bcc:
406 m['Bcc'] = ', '.join(bcc)
407 m['Bcc'] = ', '.join(bcc)
407 if opts.get('test'):
408 if opts.get('test'):
408 ui.status(_('Displaying '), subj, ' ...\n')
409 ui.status(_('Displaying '), subj, ' ...\n')
409 ui.flush()
410 ui.flush()
410 if 'PAGER' in os.environ:
411 if 'PAGER' in os.environ:
411 fp = util.popen(os.environ['PAGER'], 'w')
412 fp = util.popen(os.environ['PAGER'], 'w')
412 else:
413 else:
413 fp = ui
414 fp = ui
414 generator = email.Generator.Generator(fp, mangle_from_=False)
415 generator = email.Generator.Generator(fp, mangle_from_=False)
415 try:
416 try:
416 generator.flatten(m, 0)
417 generator.flatten(m, 0)
417 fp.write('\n')
418 fp.write('\n')
418 except IOError, inst:
419 except IOError, inst:
419 if inst.errno != errno.EPIPE:
420 if inst.errno != errno.EPIPE:
420 raise
421 raise
421 if fp is not ui:
422 if fp is not ui:
422 fp.close()
423 fp.close()
423 elif opts.get('mbox'):
424 elif opts.get('mbox'):
424 ui.status(_('Writing '), subj, ' ...\n')
425 ui.status(_('Writing '), subj, ' ...\n')
425 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
426 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
426 generator = email.Generator.Generator(fp, mangle_from_=True)
427 generator = email.Generator.Generator(fp, mangle_from_=True)
427 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
428 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
428 fp.write('From %s %s\n' % (sender_addr, date))
429 fp.write('From %s %s\n' % (sender_addr, date))
429 generator.flatten(m, 0)
430 generator.flatten(m, 0)
430 fp.write('\n\n')
431 fp.write('\n\n')
431 fp.close()
432 fp.close()
432 else:
433 else:
433 if not sendmail:
434 if not sendmail:
434 sendmail = mail.connect(ui)
435 sendmail = mail.connect(ui)
435 ui.status(_('Sending '), subj, ' ...\n')
436 ui.status(_('Sending '), subj, ' ...\n')
436 # Exim does not remove the Bcc field
437 # Exim does not remove the Bcc field
437 del m['Bcc']
438 del m['Bcc']
438 fp = cStringIO.StringIO()
439 fp = cStringIO.StringIO()
439 generator = email.Generator.Generator(fp, mangle_from_=False)
440 generator = email.Generator.Generator(fp, mangle_from_=False)
440 generator.flatten(m, 0)
441 generator.flatten(m, 0)
441 sendmail(sender, to + bcc + cc, fp.getvalue())
442 sendmail(sender, to + bcc + cc, fp.getvalue())
442
443
443 cmdtable = {
444 cmdtable = {
444 "email":
445 "email":
445 (patchbomb,
446 (patchbomb,
446 [('a', 'attach', None, _('send patches as attachments')),
447 [('a', 'attach', None, _('send patches as attachments')),
447 ('i', 'inline', None, _('send patches as inline attachments')),
448 ('i', 'inline', None, _('send patches as inline attachments')),
448 ('', 'bcc', [], _('email addresses of blind copy recipients')),
449 ('', 'bcc', [], _('email addresses of blind copy recipients')),
449 ('c', 'cc', [], _('email addresses of copy recipients')),
450 ('c', 'cc', [], _('email addresses of copy recipients')),
450 ('d', 'diffstat', None, _('add diffstat output to messages')),
451 ('d', 'diffstat', None, _('add diffstat output to messages')),
451 ('', 'date', '', _('use the given date as the sending date')),
452 ('', 'date', '', _('use the given date as the sending date')),
452 ('', 'desc', '', _('use the given file as the series description')),
453 ('', 'desc', '', _('use the given file as the series description')),
453 ('g', 'git', None, _('use git extended diff format')),
454 ('g', 'git', None, _('use git extended diff format')),
454 ('f', 'from', '', _('email address of sender')),
455 ('f', 'from', '', _('email address of sender')),
455 ('', 'plain', None, _('omit hg patch header')),
456 ('', 'plain', None, _('omit hg patch header')),
456 ('n', 'test', None, _('print messages that would be sent')),
457 ('n', 'test', None, _('print messages that would be sent')),
457 ('m', 'mbox', '',
458 ('m', 'mbox', '',
458 _('write messages to mbox file instead of sending them')),
459 _('write messages to mbox file instead of sending them')),
459 ('o', 'outgoing', None,
460 ('o', 'outgoing', None,
460 _('send changes not found in the target repository')),
461 _('send changes not found in the target repository')),
461 ('b', 'bundle', None,
462 ('b', 'bundle', None,
462 _('send changes not in target as a binary bundle')),
463 _('send changes not in target as a binary bundle')),
463 ('r', 'rev', [], _('a revision to send')),
464 ('r', 'rev', [], _('a revision to send')),
464 ('s', 'subject', '',
465 ('s', 'subject', '',
465 _('subject of first message (intro or single patch)')),
466 _('subject of first message (intro or single patch)')),
466 ('t', 'to', [], _('email addresses of recipients')),
467 ('t', 'to', [], _('email addresses of recipients')),
467 ('', 'force', None,
468 ('', 'force', None,
468 _('run even when remote repository is unrelated (with -b)')),
469 _('run even when remote repository is unrelated (with -b)')),
469 ('', 'base', [],
470 ('', 'base', [],
470 _('a base changeset to specify instead of a destination (with -b)')),
471 _('a base changeset to specify instead of a destination (with -b)')),
471 ] + commands.remoteopts,
472 ] + commands.remoteopts,
472 _('hg email [OPTION]... [DEST]...'))
473 _('hg email [OPTION]... [DEST]...'))
473 }
474 }
General Comments 0
You need to be logged in to leave comments. Login now