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