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