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