##// END OF EJS Templates
patchbomb: page patchbomb messages only if PAGER is defined....
Patrick Mezard -
r4599:88fc92b0 default
parent child Browse files
Show More
@@ -1,430 +1,435 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, your pager will be fired up once for each patchbomb message, so
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 # you can verify everything is alright.
49 # you can verify everything is alright.
49 #
50 #
50 # The "-m" (mbox) option is also very useful. Instead of previewing
51 # The "-m" (mbox) option is also very useful. Instead of previewing
51 # each patchbomb message in a pager or sending the messages directly,
52 # each patchbomb message in a pager or sending the messages directly,
52 # 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
53 # 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
54 # UNIX mbox files, i.e. with mutt:
55 # UNIX mbox files, i.e. with mutt:
55 #
56 #
56 # % mutt -R -f mbox
57 # % mutt -R -f mbox
57 #
58 #
58 # When you are previewing the patchbomb messages, you can use `formail'
59 # When you are previewing the patchbomb messages, you can use `formail'
59 # (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),
60 # to send each message out:
61 # to send each message out:
61 #
62 #
62 # % formail -s sendmail -bm -t < mbox
63 # % formail -s sendmail -bm -t < mbox
63 #
64 #
64 # 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.
65
66
66 import os, errno, socket, tempfile
67 import os, errno, socket, tempfile
67 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
68 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
68 import email.Utils, email.Encoders
69 import email.Utils, email.Encoders
69 from mercurial import cmdutil, commands, hg, mail, ui, patch, util
70 from mercurial import cmdutil, commands, hg, mail, ui, patch, util
70 from mercurial.i18n import _
71 from mercurial.i18n import _
71 from mercurial.node import *
72 from mercurial.node import *
72
73
73 def patchbomb(ui, repo, *revs, **opts):
74 def patchbomb(ui, repo, *revs, **opts):
74 '''send changesets by email
75 '''send changesets by email
75
76
76 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,
77 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]"
78 introduction, which describes the series as a whole.
79 introduction, which describes the series as a whole.
79
80
80 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
81 the first line of the changeset description as the subject text.
82 the first line of the changeset description as the subject text.
82 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
83 the changeset description. Next, (optionally) if the diffstat
84 the changeset description. Next, (optionally) if the diffstat
84 program is installed, the result of running diffstat on the patch.
85 program is installed, the result of running diffstat on the patch.
85 Finally, the patch itself, as generated by "hg export".
86 Finally, the patch itself, as generated by "hg export".
86
87
87 With --outgoing, emails will be generated for patches not
88 With --outgoing, emails will be generated for patches not
88 found in the destination repository (or only those which are
89 found in the destination repository (or only those which are
89 ancestors of the specified revisions if any are provided)
90 ancestors of the specified revisions if any are provided)
90
91
91 With --bundle, changesets are selected as for --outgoing,
92 With --bundle, changesets are selected as for --outgoing,
92 but a single email containing a binary Mercurial bundle as an
93 but a single email containing a binary Mercurial bundle as an
93 attachment will be sent.
94 attachment will be sent.
94
95
95 Examples:
96 Examples:
96
97
97 hg email -r 3000 # send patch 3000 only
98 hg email -r 3000 # send patch 3000 only
98 hg email -r 3000 -r 3001 # send patches 3000 and 3001
99 hg email -r 3000 -r 3001 # send patches 3000 and 3001
99 hg email -r 3000:3005 # send patches 3000 through 3005
100 hg email -r 3000:3005 # send patches 3000 through 3005
100 hg email 3000 # send patch 3000 (deprecated)
101 hg email 3000 # send patch 3000 (deprecated)
101
102
102 hg email -o # send all patches not in default
103 hg email -o # send all patches not in default
103 hg email -o DEST # send all patches not in DEST
104 hg email -o DEST # send all patches not in DEST
104 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
105 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
106
107
107 hg email -b # send bundle of all patches not in default
108 hg email -b # send bundle of all patches not in default
108 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
109 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
110 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
111
112
112 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.
113 See the [email] section in hgrc(5) for details.
114 See the [email] section in hgrc(5) for details.
114 '''
115 '''
115
116
116 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
117 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
117 try:
118 try:
118 # readline gives raw_input editing capabilities, but is not
119 # readline gives raw_input editing capabilities, but is not
119 # present on windows
120 # present on windows
120 import readline
121 import readline
121 except ImportError: pass
122 except ImportError: pass
122
123
123 if default: prompt += ' [%s]' % default
124 if default: prompt += ' [%s]' % default
124 prompt += rest
125 prompt += rest
125 while True:
126 while True:
126 r = raw_input(prompt)
127 r = raw_input(prompt)
127 if r: return r
128 if r: return r
128 if default is not None: return default
129 if default is not None: return default
129 if empty_ok: return r
130 if empty_ok: return r
130 ui.warn(_('Please enter a valid value.\n'))
131 ui.warn(_('Please enter a valid value.\n'))
131
132
132 def confirm(s):
133 def confirm(s):
133 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
134 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
134 raise ValueError
135 raise ValueError
135
136
136 def cdiffstat(summary, patchlines):
137 def cdiffstat(summary, patchlines):
137 s = patch.diffstat(patchlines)
138 s = patch.diffstat(patchlines)
138 if s:
139 if s:
139 if summary:
140 if summary:
140 ui.write(summary, '\n')
141 ui.write(summary, '\n')
141 ui.write(s, '\n')
142 ui.write(s, '\n')
142 confirm(_('Does the diffstat above look okay'))
143 confirm(_('Does the diffstat above look okay'))
143 return s
144 return s
144
145
145 def makepatch(patch, idx, total):
146 def makepatch(patch, idx, total):
146 desc = []
147 desc = []
147 node = None
148 node = None
148 body = ''
149 body = ''
149 for line in patch:
150 for line in patch:
150 if line.startswith('#'):
151 if line.startswith('#'):
151 if line.startswith('# Node ID'): node = line.split()[-1]
152 if line.startswith('# Node ID'): node = line.split()[-1]
152 continue
153 continue
153 if (line.startswith('diff -r')
154 if (line.startswith('diff -r')
154 or line.startswith('diff --git')):
155 or line.startswith('diff --git')):
155 break
156 break
156 desc.append(line)
157 desc.append(line)
157 if not node: raise ValueError
158 if not node: raise ValueError
158
159
159 #body = ('\n'.join(desc[1:]).strip() or
160 #body = ('\n'.join(desc[1:]).strip() or
160 # 'Patch subject is complete summary.')
161 # 'Patch subject is complete summary.')
161 #body += '\n\n\n'
162 #body += '\n\n\n'
162
163
163 if opts['plain']:
164 if opts['plain']:
164 while patch and patch[0].startswith('# '): patch.pop(0)
165 while patch and patch[0].startswith('# '): patch.pop(0)
165 if patch: patch.pop(0)
166 if patch: patch.pop(0)
166 while patch and not patch[0].strip(): patch.pop(0)
167 while patch and not patch[0].strip(): patch.pop(0)
167 if opts['diffstat']:
168 if opts['diffstat']:
168 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
169 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
169 if opts['attach']:
170 if opts['attach']:
170 msg = email.MIMEMultipart.MIMEMultipart()
171 msg = email.MIMEMultipart.MIMEMultipart()
171 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
172 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
172 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
173 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
173 binnode = bin(node)
174 binnode = bin(node)
174 # 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
175 patchname = [t for t in repo.nodetags(binnode)
176 patchname = [t for t in repo.nodetags(binnode)
176 if t.endswith('.patch') or t.endswith('.diff')]
177 if t.endswith('.patch') or t.endswith('.diff')]
177 if patchname:
178 if patchname:
178 patchname = patchname[0]
179 patchname = patchname[0]
179 elif total > 1:
180 elif total > 1:
180 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
181 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
181 binnode, idx, total)
182 binnode, idx, total)
182 else:
183 else:
183 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
184 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
184 p['Content-Disposition'] = 'inline; filename=' + patchname
185 p['Content-Disposition'] = 'inline; filename=' + patchname
185 msg.attach(p)
186 msg.attach(p)
186 else:
187 else:
187 body += '\n'.join(patch)
188 body += '\n'.join(patch)
188 msg = email.MIMEText.MIMEText(body)
189 msg = email.MIMEText.MIMEText(body)
189
190
190 subj = desc[0].strip().rstrip('. ')
191 subj = desc[0].strip().rstrip('. ')
191 if total == 1:
192 if total == 1:
192 subj = '[PATCH] ' + (opts['subject'] or subj)
193 subj = '[PATCH] ' + (opts['subject'] or subj)
193 else:
194 else:
194 tlen = len(str(total))
195 tlen = len(str(total))
195 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
196 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
196 msg['Subject'] = subj
197 msg['Subject'] = subj
197 msg['X-Mercurial-Node'] = node
198 msg['X-Mercurial-Node'] = node
198 return msg
199 return msg
199
200
200 def outgoing(dest, revs):
201 def outgoing(dest, revs):
201 '''Return the revisions present locally but not in dest'''
202 '''Return the revisions present locally but not in dest'''
202 dest = ui.expandpath(dest or 'default-push', dest or 'default')
203 dest = ui.expandpath(dest or 'default-push', dest or 'default')
203 revs = [repo.lookup(rev) for rev in revs]
204 revs = [repo.lookup(rev) for rev in revs]
204 other = hg.repository(ui, dest)
205 other = hg.repository(ui, dest)
205 ui.status(_('comparing with %s\n') % dest)
206 ui.status(_('comparing with %s\n') % dest)
206 o = repo.findoutgoing(other)
207 o = repo.findoutgoing(other)
207 if not o:
208 if not o:
208 ui.status(_("no changes found\n"))
209 ui.status(_("no changes found\n"))
209 return []
210 return []
210 o = repo.changelog.nodesbetween(o, revs or None)[0]
211 o = repo.changelog.nodesbetween(o, revs or None)[0]
211 return [str(repo.changelog.rev(r)) for r in o]
212 return [str(repo.changelog.rev(r)) for r in o]
212
213
213 def getbundle(dest):
214 def getbundle(dest):
214 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
215 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
215 tmpfn = os.path.join(tmpdir, 'bundle')
216 tmpfn = os.path.join(tmpdir, 'bundle')
216 try:
217 try:
217 commands.bundle(ui, repo, tmpfn, dest, **opts)
218 commands.bundle(ui, repo, tmpfn, dest, **opts)
218 return open(tmpfn).read()
219 return open(tmpfn).read()
219 finally:
220 finally:
220 try:
221 try:
221 os.unlink(tmpfn)
222 os.unlink(tmpfn)
222 except:
223 except:
223 pass
224 pass
224 os.rmdir(tmpdir)
225 os.rmdir(tmpdir)
225
226
226 really_sending = not (opts['test'] or opts['mbox'])
227 really_sending = not (opts['test'] or opts['mbox'])
227
228
228 if really_sending:
229 if really_sending:
229 mail.validateconfig(ui)
230 mail.validateconfig(ui)
230
231
231 if not (revs or opts.get('rev') or opts.get('outgoing')):
232 if not (revs or opts.get('rev') or opts.get('outgoing')):
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 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
309 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
309 body = ui.edit(body, sender)
310 body = ui.edit(body, sender)
310
311
311 msg = email.MIMEText.MIMEText(body)
312 msg = email.MIMEText.MIMEText(body)
312 msg['Subject'] = subj
313 msg['Subject'] = subj
313
314
314 msgs.insert(0, msg)
315 msgs.insert(0, msg)
315 return msgs
316 return msgs
316
317
317 def getbundlemsgs(bundle):
318 def getbundlemsgs(bundle):
318 subj = opts['subject'] or \
319 subj = opts['subject'] or \
319 prompt('Subject:', default='A bundle for your repository')
320 prompt('Subject:', default='A bundle for your repository')
320 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
321 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
321 body = ui.edit('', sender)
322 body = ui.edit('', sender)
322
323
323 msg = email.MIMEMultipart.MIMEMultipart()
324 msg = email.MIMEMultipart.MIMEMultipart()
324 if body:
325 if body:
325 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
326 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
326 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
327 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
327 datapart.set_payload(bundle)
328 datapart.set_payload(bundle)
328 datapart.add_header('Content-Disposition', 'attachment',
329 datapart.add_header('Content-Disposition', 'attachment',
329 filename='bundle.hg')
330 filename='bundle.hg')
330 email.Encoders.encode_base64(datapart)
331 email.Encoders.encode_base64(datapart)
331 msg.attach(datapart)
332 msg.attach(datapart)
332 msg['Subject'] = subj
333 msg['Subject'] = subj
333 return [msg]
334 return [msg]
334
335
335 sender = (opts['from'] or ui.config('email', 'from') or
336 sender = (opts['from'] or ui.config('email', 'from') or
336 ui.config('patchbomb', 'from') or
337 ui.config('patchbomb', 'from') or
337 prompt('From', ui.username()))
338 prompt('From', ui.username()))
338
339
339 if opts.get('bundle'):
340 if opts.get('bundle'):
340 msgs = getbundlemsgs(getbundle(dest))
341 msgs = getbundlemsgs(getbundle(dest))
341 else:
342 else:
342 msgs = getexportmsgs()
343 msgs = getexportmsgs()
343
344
344 def getaddrs(opt, prpt, default = None):
345 def getaddrs(opt, prpt, default = None):
345 addrs = opts[opt] or (ui.config('email', opt) or
346 addrs = opts[opt] or (ui.config('email', opt) or
346 ui.config('patchbomb', opt) or
347 ui.config('patchbomb', opt) or
347 prompt(prpt, default = default)).split(',')
348 prompt(prpt, default = default)).split(',')
348 return [a.strip() for a in addrs if a.strip()]
349 return [a.strip() for a in addrs if a.strip()]
349
350
350 to = getaddrs('to', 'To')
351 to = getaddrs('to', 'To')
351 cc = getaddrs('cc', 'Cc', '')
352 cc = getaddrs('cc', 'Cc', '')
352
353
353 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
354 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
354 ui.config('patchbomb', 'bcc') or '').split(',')
355 ui.config('patchbomb', 'bcc') or '').split(',')
355 bcc = [a.strip() for a in bcc if a.strip()]
356 bcc = [a.strip() for a in bcc if a.strip()]
356
357
357 ui.write('\n')
358 ui.write('\n')
358
359
359 if really_sending:
360 if really_sending:
360 mailer = mail.connect(ui)
361 mailer = mail.connect(ui)
361 parent = None
362 parent = None
362
363
363 sender_addr = email.Utils.parseaddr(sender)[1]
364 sender_addr = email.Utils.parseaddr(sender)[1]
364 for m in msgs:
365 for m in msgs:
365 try:
366 try:
366 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
367 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
367 except TypeError:
368 except TypeError:
368 m['Message-Id'] = genmsgid('patchbomb')
369 m['Message-Id'] = genmsgid('patchbomb')
369 if parent:
370 if parent:
370 m['In-Reply-To'] = parent
371 m['In-Reply-To'] = parent
371 else:
372 else:
372 parent = m['Message-Id']
373 parent = m['Message-Id']
373 m['Date'] = util.datestr(date=start_time,
374 m['Date'] = util.datestr(date=start_time,
374 format="%a, %d %b %Y %H:%M:%S", timezone=True)
375 format="%a, %d %b %Y %H:%M:%S", timezone=True)
375
376
376 start_time = (start_time[0] + 1, start_time[1])
377 start_time = (start_time[0] + 1, start_time[1])
377 m['From'] = sender
378 m['From'] = sender
378 m['To'] = ', '.join(to)
379 m['To'] = ', '.join(to)
379 if cc: m['Cc'] = ', '.join(cc)
380 if cc: m['Cc'] = ', '.join(cc)
380 if bcc: m['Bcc'] = ', '.join(bcc)
381 if bcc: m['Bcc'] = ', '.join(bcc)
381 if opts['test']:
382 if opts['test']:
382 ui.status('Displaying ', m['Subject'], ' ...\n')
383 ui.status('Displaying ', m['Subject'], ' ...\n')
383 ui.flush()
384 ui.flush()
384 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
385 if 'PAGER' in os.environ:
386 fp = os.popen(os['PAGER'], 'w')
387 else:
388 fp = ui
385 try:
389 try:
386 fp.write(m.as_string(0))
390 fp.write(m.as_string(0))
387 fp.write('\n')
391 fp.write('\n')
388 except IOError, inst:
392 except IOError, inst:
389 if inst.errno != errno.EPIPE:
393 if inst.errno != errno.EPIPE:
390 raise
394 raise
391 fp.close()
395 if fp is not ui:
396 fp.close()
392 elif opts['mbox']:
397 elif opts['mbox']:
393 ui.status('Writing ', m['Subject'], ' ...\n')
398 ui.status('Writing ', m['Subject'], ' ...\n')
394 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
399 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
395 date = util.datestr(date=start_time,
400 date = util.datestr(date=start_time,
396 format='%a %b %d %H:%M:%S %Y', timezone=False)
401 format='%a %b %d %H:%M:%S %Y', timezone=False)
397 fp.write('From %s %s\n' % (sender_addr, date))
402 fp.write('From %s %s\n' % (sender_addr, date))
398 fp.write(m.as_string(0))
403 fp.write(m.as_string(0))
399 fp.write('\n\n')
404 fp.write('\n\n')
400 fp.close()
405 fp.close()
401 else:
406 else:
402 ui.status('Sending ', m['Subject'], ' ...\n')
407 ui.status('Sending ', m['Subject'], ' ...\n')
403 # Exim does not remove the Bcc field
408 # Exim does not remove the Bcc field
404 del m['Bcc']
409 del m['Bcc']
405 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
410 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
406
411
407 cmdtable = {
412 cmdtable = {
408 'email':
413 'email':
409 (patchbomb,
414 (patchbomb,
410 [('a', 'attach', None, 'send patches as inline attachments'),
415 [('a', 'attach', None, 'send patches as inline attachments'),
411 ('', 'bcc', [], 'email addresses of blind copy recipients'),
416 ('', 'bcc', [], 'email addresses of blind copy recipients'),
412 ('c', 'cc', [], 'email addresses of copy recipients'),
417 ('c', 'cc', [], 'email addresses of copy recipients'),
413 ('d', 'diffstat', None, 'add diffstat output to messages'),
418 ('d', 'diffstat', None, 'add diffstat output to messages'),
414 ('', 'date', '', _('use the given date as the sending date')),
419 ('', 'date', '', _('use the given date as the sending date')),
415 ('g', 'git', None, _('use git extended diff format')),
420 ('g', 'git', None, _('use git extended diff format')),
416 ('f', 'from', '', 'email address of sender'),
421 ('f', 'from', '', 'email address of sender'),
417 ('', 'plain', None, 'omit hg patch header'),
422 ('', 'plain', None, 'omit hg patch header'),
418 ('n', 'test', None, 'print messages that would be sent'),
423 ('n', 'test', None, 'print messages that would be sent'),
419 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
424 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
420 ('o', 'outgoing', None, _('send changes not found in the target repository')),
425 ('o', 'outgoing', None, _('send changes not found in the target repository')),
421 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
426 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
422 ('r', 'rev', [], _('a revision to send')),
427 ('r', 'rev', [], _('a revision to send')),
423 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
428 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
424 ('t', 'to', [], 'email addresses of recipients'),
429 ('t', 'to', [], 'email addresses of recipients'),
425 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
430 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
426 ('', 'base', [],
431 ('', 'base', [],
427 _('a base changeset to specify instead of a destination (with -b)'))]
432 _('a base changeset to specify instead of a destination (with -b)'))]
428 + commands.remoteopts,
433 + commands.remoteopts,
429 "hg email [OPTION]... [DEST]...")
434 "hg email [OPTION]... [DEST]...")
430 }
435 }
General Comments 0
You need to be logged in to leave comments. Login now