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