##// END OF EJS Templates
patchbomb: make diffstat usage clearer...
Cédric Duval -
r8473:69f51fd9 default
parent child Browse files
Show More
@@ -1,501 +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 all of its predecessors using the In-Reply-To
23 Each message refers to all of its predecessors 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 For each changeset, you will be prompted with a diffstat summary and
27 For each changeset, you will be prompted with a diffstat summary and
28 the changeset summary, so you can be sure you are sending the right
28 the changeset summary, so you can be sure you are sending the right
29 changes.
29 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 an 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 PAGER environment variable
53 is set, your pager will be fired up once for each patchbomb message,
53 is set, your pager will be fired up once for each patchbomb message,
54 so you can verify everything is alright.
54 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 compatable 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, the result of running diffstat on the patch. Finally,
192 installed and -d/--diffstat is used, the result of running
193 the patch itself, as generated by "hg export".
193 diffstat on the patch. Finally, the patch itself, as generated by
194 "hg export".
194
195
195 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
196 easy reviewing. Using the -a/--attach option will instead create
197 easy reviewing. Using the -a/--attach option will instead create
197 an attachment for the patch. With -i/--inline an inline attachment
198 an attachment for the patch. With -i/--inline an inline attachment
198 will be created.
199 will be created.
199
200
200 With -o/--outgoing, emails will be generated for patches not found
201 With -o/--outgoing, emails will be generated for patches not found
201 in the destination repository (or only those which are ancestors
202 in the destination repository (or only those which are ancestors
202 of the specified revisions if any are provided)
203 of the specified revisions if any are provided)
203
204
204 With -b/--bundle, changesets are selected as for --outgoing, but a
205 With -b/--bundle, changesets are selected as for --outgoing, but a
205 single email containing a binary Mercurial bundle as an attachment
206 single email containing a binary Mercurial bundle as an attachment
206 will be sent.
207 will be sent.
207
208
208 Examples:
209 Examples:
209
210
210 hg email -r 3000 # send patch 3000 only
211 hg email -r 3000 # send patch 3000 only
211 hg email -r 3000 -r 3001 # send patches 3000 and 3001
212 hg email -r 3000 -r 3001 # send patches 3000 and 3001
212 hg email -r 3000:3005 # send patches 3000 through 3005
213 hg email -r 3000:3005 # send patches 3000 through 3005
213 hg email 3000 # send patch 3000 (deprecated)
214 hg email 3000 # send patch 3000 (deprecated)
214
215
215 hg email -o # send all patches not in default
216 hg email -o # send all patches not in default
216 hg email -o DEST # send all patches not in DEST
217 hg email -o DEST # send all patches not in DEST
217 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
218 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
219
220
220 hg email -b # send bundle of all patches not in default
221 hg email -b # send bundle of all patches not in default
221 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
222 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
223 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
224
225
225 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
226 hgrc. See the [email] section in hgrc(5) for details.
227 hgrc. See the [email] section in hgrc(5) for details.
227 '''
228 '''
228
229
229 _charsets = mail._charsets(ui)
230 _charsets = mail._charsets(ui)
230
231
231 def outgoing(dest, revs):
232 def outgoing(dest, revs):
232 '''Return the revisions present locally but not in dest'''
233 '''Return the revisions present locally but not in dest'''
233 dest = ui.expandpath(dest or 'default-push', dest or 'default')
234 dest = ui.expandpath(dest or 'default-push', dest or 'default')
234 revs = [repo.lookup(rev) for rev in revs]
235 revs = [repo.lookup(rev) for rev in revs]
235 other = hg.repository(cmdutil.remoteui(repo, opts), dest)
236 other = hg.repository(cmdutil.remoteui(repo, opts), dest)
236 ui.status(_('comparing with %s\n') % dest)
237 ui.status(_('comparing with %s\n') % dest)
237 o = repo.findoutgoing(other)
238 o = repo.findoutgoing(other)
238 if not o:
239 if not o:
239 ui.status(_("no changes found\n"))
240 ui.status(_("no changes found\n"))
240 return []
241 return []
241 o = repo.changelog.nodesbetween(o, revs or None)[0]
242 o = repo.changelog.nodesbetween(o, revs or None)[0]
242 return [str(repo.changelog.rev(r)) for r in o]
243 return [str(repo.changelog.rev(r)) for r in o]
243
244
244 def getpatches(revs):
245 def getpatches(revs):
245 for r in cmdutil.revrange(repo, revs):
246 for r in cmdutil.revrange(repo, revs):
246 output = cStringIO.StringIO()
247 output = cStringIO.StringIO()
247 patch.export(repo, [r], fp=output,
248 patch.export(repo, [r], fp=output,
248 opts=patch.diffopts(ui, opts))
249 opts=patch.diffopts(ui, opts))
249 yield output.getvalue().split('\n')
250 yield output.getvalue().split('\n')
250
251
251 def getbundle(dest):
252 def getbundle(dest):
252 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
253 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
253 tmpfn = os.path.join(tmpdir, 'bundle')
254 tmpfn = os.path.join(tmpdir, 'bundle')
254 try:
255 try:
255 commands.bundle(ui, repo, tmpfn, dest, **opts)
256 commands.bundle(ui, repo, tmpfn, dest, **opts)
256 return open(tmpfn, 'rb').read()
257 return open(tmpfn, 'rb').read()
257 finally:
258 finally:
258 try:
259 try:
259 os.unlink(tmpfn)
260 os.unlink(tmpfn)
260 except:
261 except:
261 pass
262 pass
262 os.rmdir(tmpdir)
263 os.rmdir(tmpdir)
263
264
264 if not (opts.get('test') or opts.get('mbox')):
265 if not (opts.get('test') or opts.get('mbox')):
265 # really sending
266 # really sending
266 mail.validateconfig(ui)
267 mail.validateconfig(ui)
267
268
268 if not (revs or opts.get('rev')
269 if not (revs or opts.get('rev')
269 or opts.get('outgoing') or opts.get('bundle')
270 or opts.get('outgoing') or opts.get('bundle')
270 or opts.get('patches')):
271 or opts.get('patches')):
271 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'))
272
273
273 if opts.get('outgoing') and opts.get('bundle'):
274 if opts.get('outgoing') and opts.get('bundle'):
274 raise util.Abort(_("--outgoing mode always on with --bundle;"
275 raise util.Abort(_("--outgoing mode always on with --bundle;"
275 " do not re-specify --outgoing"))
276 " do not re-specify --outgoing"))
276
277
277 if opts.get('outgoing') or opts.get('bundle'):
278 if opts.get('outgoing') or opts.get('bundle'):
278 if len(revs) > 1:
279 if len(revs) > 1:
279 raise util.Abort(_("too many destinations"))
280 raise util.Abort(_("too many destinations"))
280 dest = revs and revs[0] or None
281 dest = revs and revs[0] or None
281 revs = []
282 revs = []
282
283
283 if opts.get('rev'):
284 if opts.get('rev'):
284 if revs:
285 if revs:
285 raise util.Abort(_('use only one form to specify the revision'))
286 raise util.Abort(_('use only one form to specify the revision'))
286 revs = opts.get('rev')
287 revs = opts.get('rev')
287
288
288 if opts.get('outgoing'):
289 if opts.get('outgoing'):
289 revs = outgoing(dest, opts.get('rev'))
290 revs = outgoing(dest, opts.get('rev'))
290 if opts.get('bundle'):
291 if opts.get('bundle'):
291 opts['revs'] = revs
292 opts['revs'] = revs
292
293
293 # start
294 # start
294 if opts.get('date'):
295 if opts.get('date'):
295 start_time = util.parsedate(opts.get('date'))
296 start_time = util.parsedate(opts.get('date'))
296 else:
297 else:
297 start_time = util.makedate()
298 start_time = util.makedate()
298
299
299 def genmsgid(id):
300 def genmsgid(id):
300 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())
301
302
302 def getdescription(body, sender):
303 def getdescription(body, sender):
303 if opts.get('desc'):
304 if opts.get('desc'):
304 body = open(opts.get('desc')).read()
305 body = open(opts.get('desc')).read()
305 else:
306 else:
306 ui.write(_('\nWrite the introductory message for the '
307 ui.write(_('\nWrite the introductory message for the '
307 'patch series.\n\n'))
308 'patch series.\n\n'))
308 body = ui.edit(body, sender)
309 body = ui.edit(body, sender)
309 return body
310 return body
310
311
311 def getpatchmsgs(patches, patchnames=None):
312 def getpatchmsgs(patches, patchnames=None):
312 jumbo = []
313 jumbo = []
313 msgs = []
314 msgs = []
314
315
315 ui.write(_('This patch series consists of %d patches.\n\n')
316 ui.write(_('This patch series consists of %d patches.\n\n')
316 % len(patches))
317 % len(patches))
317
318
318 name = None
319 name = None
319 for i, p in enumerate(patches):
320 for i, p in enumerate(patches):
320 jumbo.extend(p)
321 jumbo.extend(p)
321 if patchnames:
322 if patchnames:
322 name = patchnames[i]
323 name = patchnames[i]
323 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
324 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
324 len(patches), name)
325 len(patches), name)
325 msgs.append(msg)
326 msgs.append(msg)
326
327
327 if len(patches) > 1 or opts.get('intro'):
328 if len(patches) > 1 or opts.get('intro'):
328 tlen = len(str(len(patches)))
329 tlen = len(str(len(patches)))
329
330
330 subj = '[PATCH %0*d of %d] %s' % (
331 subj = '[PATCH %0*d of %d] %s' % (
331 tlen, 0, len(patches),
332 tlen, 0, len(patches),
332 opts.get('subject') or
333 opts.get('subject') or
333 prompt(ui, 'Subject:',
334 prompt(ui, 'Subject:',
334 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
335 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
335
336
336 body = ''
337 body = ''
337 if opts.get('diffstat'):
338 if opts.get('diffstat'):
338 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
339 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
339 if d:
340 if d:
340 body = '\n' + d
341 body = '\n' + d
341
342
342 body = getdescription(body, sender)
343 body = getdescription(body, sender)
343 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
344 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
344 msg['Subject'] = mail.headencode(ui, subj, _charsets,
345 msg['Subject'] = mail.headencode(ui, subj, _charsets,
345 opts.get('test'))
346 opts.get('test'))
346
347
347 msgs.insert(0, (msg, subj))
348 msgs.insert(0, (msg, subj))
348 return msgs
349 return msgs
349
350
350 def getbundlemsgs(bundle):
351 def getbundlemsgs(bundle):
351 subj = (opts.get('subject')
352 subj = (opts.get('subject')
352 or prompt(ui, 'Subject:', 'A bundle for your repository'))
353 or prompt(ui, 'Subject:', 'A bundle for your repository'))
353
354
354 body = getdescription('', sender)
355 body = getdescription('', sender)
355 msg = email.MIMEMultipart.MIMEMultipart()
356 msg = email.MIMEMultipart.MIMEMultipart()
356 if body:
357 if body:
357 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
358 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
358 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
359 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
359 datapart.set_payload(bundle)
360 datapart.set_payload(bundle)
360 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
361 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
361 datapart.add_header('Content-Disposition', 'attachment',
362 datapart.add_header('Content-Disposition', 'attachment',
362 filename=bundlename)
363 filename=bundlename)
363 email.Encoders.encode_base64(datapart)
364 email.Encoders.encode_base64(datapart)
364 msg.attach(datapart)
365 msg.attach(datapart)
365 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
366 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
366 return [(msg, subj)]
367 return [(msg, subj)]
367
368
368 sender = (opts.get('from') or ui.config('email', 'from') or
369 sender = (opts.get('from') or ui.config('email', 'from') or
369 ui.config('patchbomb', 'from') or
370 ui.config('patchbomb', 'from') or
370 prompt(ui, 'From', ui.username()))
371 prompt(ui, 'From', ui.username()))
371
372
372 # internal option used by pbranches
373 # internal option used by pbranches
373 patches = opts.get('patches')
374 patches = opts.get('patches')
374 if patches:
375 if patches:
375 msgs = getpatchmsgs(patches, opts.get('patchnames'))
376 msgs = getpatchmsgs(patches, opts.get('patchnames'))
376 elif opts.get('bundle'):
377 elif opts.get('bundle'):
377 msgs = getbundlemsgs(getbundle(dest))
378 msgs = getbundlemsgs(getbundle(dest))
378 else:
379 else:
379 msgs = getpatchmsgs(list(getpatches(revs)))
380 msgs = getpatchmsgs(list(getpatches(revs)))
380
381
381 def getaddrs(opt, prpt, default = None):
382 def getaddrs(opt, prpt, default = None):
382 addrs = opts.get(opt) or (ui.config('email', opt) or
383 addrs = opts.get(opt) or (ui.config('email', opt) or
383 ui.config('patchbomb', opt) or
384 ui.config('patchbomb', opt) or
384 prompt(ui, prpt, default)).split(',')
385 prompt(ui, prpt, default)).split(',')
385 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
386 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
386 for a in addrs if a.strip()]
387 for a in addrs if a.strip()]
387
388
388 to = getaddrs('to', 'To')
389 to = getaddrs('to', 'To')
389 cc = getaddrs('cc', 'Cc', '')
390 cc = getaddrs('cc', 'Cc', '')
390
391
391 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
392 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
392 ui.config('patchbomb', 'bcc') or '').split(',')
393 ui.config('patchbomb', 'bcc') or '').split(',')
393 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
394 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
394 for a in bcc if a.strip()]
395 for a in bcc if a.strip()]
395
396
396 ui.write('\n')
397 ui.write('\n')
397
398
398 parent = opts.get('in_reply_to') or None
399 parent = opts.get('in_reply_to') or None
399
400
400 sender_addr = email.Utils.parseaddr(sender)[1]
401 sender_addr = email.Utils.parseaddr(sender)[1]
401 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
402 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
402 sendmail = None
403 sendmail = None
403 for m, subj in msgs:
404 for m, subj in msgs:
404 try:
405 try:
405 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
406 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
406 except TypeError:
407 except TypeError:
407 m['Message-Id'] = genmsgid('patchbomb')
408 m['Message-Id'] = genmsgid('patchbomb')
408 if parent:
409 if parent:
409 m['In-Reply-To'] = parent
410 m['In-Reply-To'] = parent
410 m['References'] = parent
411 m['References'] = parent
411 else:
412 else:
412 parent = m['Message-Id']
413 parent = m['Message-Id']
413 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
414 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
414 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")
415
416
416 start_time = (start_time[0] + 1, start_time[1])
417 start_time = (start_time[0] + 1, start_time[1])
417 m['From'] = sender
418 m['From'] = sender
418 m['To'] = ', '.join(to)
419 m['To'] = ', '.join(to)
419 if cc:
420 if cc:
420 m['Cc'] = ', '.join(cc)
421 m['Cc'] = ', '.join(cc)
421 if bcc:
422 if bcc:
422 m['Bcc'] = ', '.join(bcc)
423 m['Bcc'] = ', '.join(bcc)
423 if opts.get('test'):
424 if opts.get('test'):
424 ui.status(_('Displaying '), subj, ' ...\n')
425 ui.status(_('Displaying '), subj, ' ...\n')
425 ui.flush()
426 ui.flush()
426 if 'PAGER' in os.environ:
427 if 'PAGER' in os.environ:
427 fp = util.popen(os.environ['PAGER'], 'w')
428 fp = util.popen(os.environ['PAGER'], 'w')
428 else:
429 else:
429 fp = ui
430 fp = ui
430 generator = email.Generator.Generator(fp, mangle_from_=False)
431 generator = email.Generator.Generator(fp, mangle_from_=False)
431 try:
432 try:
432 generator.flatten(m, 0)
433 generator.flatten(m, 0)
433 fp.write('\n')
434 fp.write('\n')
434 except IOError, inst:
435 except IOError, inst:
435 if inst.errno != errno.EPIPE:
436 if inst.errno != errno.EPIPE:
436 raise
437 raise
437 if fp is not ui:
438 if fp is not ui:
438 fp.close()
439 fp.close()
439 elif opts.get('mbox'):
440 elif opts.get('mbox'):
440 ui.status(_('Writing '), subj, ' ...\n')
441 ui.status(_('Writing '), subj, ' ...\n')
441 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+')
442 generator = email.Generator.Generator(fp, mangle_from_=True)
443 generator = email.Generator.Generator(fp, mangle_from_=True)
443 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')
444 fp.write('From %s %s\n' % (sender_addr, date))
445 fp.write('From %s %s\n' % (sender_addr, date))
445 generator.flatten(m, 0)
446 generator.flatten(m, 0)
446 fp.write('\n\n')
447 fp.write('\n\n')
447 fp.close()
448 fp.close()
448 else:
449 else:
449 if not sendmail:
450 if not sendmail:
450 sendmail = mail.connect(ui)
451 sendmail = mail.connect(ui)
451 ui.status(_('Sending '), subj, ' ...\n')
452 ui.status(_('Sending '), subj, ' ...\n')
452 # Exim does not remove the Bcc field
453 # Exim does not remove the Bcc field
453 del m['Bcc']
454 del m['Bcc']
454 fp = cStringIO.StringIO()
455 fp = cStringIO.StringIO()
455 generator = email.Generator.Generator(fp, mangle_from_=False)
456 generator = email.Generator.Generator(fp, mangle_from_=False)
456 generator.flatten(m, 0)
457 generator.flatten(m, 0)
457 sendmail(sender, to + bcc + cc, fp.getvalue())
458 sendmail(sender, to + bcc + cc, fp.getvalue())
458
459
459 emailopts = [
460 emailopts = [
460 ('a', 'attach', None, _('send patches as attachments')),
461 ('a', 'attach', None, _('send patches as attachments')),
461 ('i', 'inline', None, _('send patches as inline attachments')),
462 ('i', 'inline', None, _('send patches as inline attachments')),
462 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
463 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
463 ('c', 'cc', [], _('email addresses of copy recipients')),
464 ('c', 'cc', [], _('email addresses of copy recipients')),
464 ('d', 'diffstat', None, _('add diffstat output to messages')),
465 ('d', 'diffstat', None, _('add diffstat output to messages')),
465 ('', 'date', '', _('use the given date as the sending date')),
466 ('', 'date', '', _('use the given date as the sending date')),
466 ('', 'desc', '', _('use the given file as the series description')),
467 ('', 'desc', '', _('use the given file as the series description')),
467 ('f', 'from', '', _('email address of sender')),
468 ('f', 'from', '', _('email address of sender')),
468 ('n', 'test', None, _('print messages that would be sent')),
469 ('n', 'test', None, _('print messages that would be sent')),
469 ('m', 'mbox', '',
470 ('m', 'mbox', '',
470 _('write messages to mbox file instead of sending them')),
471 _('write messages to mbox file instead of sending them')),
471 ('s', 'subject', '',
472 ('s', 'subject', '',
472 _('subject of first message (intro or single patch)')),
473 _('subject of first message (intro or single patch)')),
473 ('', 'in-reply-to', '',
474 ('', 'in-reply-to', '',
474 _('message identifier to reply to')),
475 _('message identifier to reply to')),
475 ('t', 'to', [], _('email addresses of recipients')),
476 ('t', 'to', [], _('email addresses of recipients')),
476 ]
477 ]
477
478
478
479
479 cmdtable = {
480 cmdtable = {
480 "email":
481 "email":
481 (patchbomb,
482 (patchbomb,
482 [('g', 'git', None, _('use git extended diff format')),
483 [('g', 'git', None, _('use git extended diff format')),
483 ('', 'plain', None, _('omit hg patch header')),
484 ('', 'plain', None, _('omit hg patch header')),
484 ('o', 'outgoing', None,
485 ('o', 'outgoing', None,
485 _('send changes not found in the target repository')),
486 _('send changes not found in the target repository')),
486 ('b', 'bundle', None,
487 ('b', 'bundle', None,
487 _('send changes not in target as a binary bundle')),
488 _('send changes not in target as a binary bundle')),
488 ('', 'bundlename', 'bundle',
489 ('', 'bundlename', 'bundle',
489 _('file name of the bundle attachment')),
490 _('file name of the bundle attachment')),
490 ('r', 'rev', [], _('a revision to send')),
491 ('r', 'rev', [], _('a revision to send')),
491 ('', 'force', None,
492 ('', 'force', None,
492 _('run even when remote repository is unrelated '
493 _('run even when remote repository is unrelated '
493 '(with -b/--bundle)')),
494 '(with -b/--bundle)')),
494 ('', 'base', [],
495 ('', 'base', [],
495 _('a base changeset to specify instead of a destination '
496 _('a base changeset to specify instead of a destination '
496 '(with -b/--bundle)')),
497 '(with -b/--bundle)')),
497 ('', 'intro', None,
498 ('', 'intro', None,
498 _('send an introduction email for a single patch')),
499 _('send an introduction email for a single patch')),
499 ] + emailopts + commands.remoteopts,
500 ] + emailopts + commands.remoteopts,
500 _('hg email [OPTION]... [DEST]...'))
501 _('hg email [OPTION]... [DEST]...'))
501 }
502 }
General Comments 0
You need to be logged in to leave comments. Login now