##// END OF EJS Templates
patchbomb: fix PAGER buglet introduced in 88fc92b0b821
Brendan Cully -
r4600:64c415d2 default
parent child Browse files
Show More
@@ -1,435 +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, 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 try:
118 try:
119 # readline gives raw_input editing capabilities, but is not
119 # readline gives raw_input editing capabilities, but is not
120 # present on windows
120 # present on windows
121 import readline
121 import readline
122 except ImportError: pass
122 except ImportError: pass
123
123
124 if default: prompt += ' [%s]' % default
124 if default: prompt += ' [%s]' % default
125 prompt += rest
125 prompt += rest
126 while True:
126 while True:
127 r = raw_input(prompt)
127 r = raw_input(prompt)
128 if r: return r
128 if r: return r
129 if default is not None: return default
129 if default is not None: return default
130 if empty_ok: return r
130 if empty_ok: 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):
133 def confirm(s):
134 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
134 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
135 raise ValueError
135 raise ValueError
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 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 really_sending = not (opts['test'] or opts['mbox'])
227 really_sending = not (opts['test'] or opts['mbox'])
228
228
229 if really_sending:
229 if really_sending:
230 mail.validateconfig(ui)
230 mail.validateconfig(ui)
231
231
232 if not (revs or opts.get('rev') or opts.get('outgoing')):
232 if not (revs or opts.get('rev') or opts.get('outgoing')):
233 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'))
234
234
235 cmdutil.setremoteconfig(ui, opts)
235 cmdutil.setremoteconfig(ui, opts)
236 if opts.get('outgoing') and opts.get('bundle'):
236 if opts.get('outgoing') and opts.get('bundle'):
237 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"))
238
238
239 if opts.get('outgoing') or opts.get('bundle'):
239 if opts.get('outgoing') or opts.get('bundle'):
240 if len(revs) > 1:
240 if len(revs) > 1:
241 raise util.Abort(_("too many destinations"))
241 raise util.Abort(_("too many destinations"))
242 dest = revs and revs[0] or None
242 dest = revs and revs[0] or None
243 revs = []
243 revs = []
244
244
245 if opts.get('rev'):
245 if opts.get('rev'):
246 if revs:
246 if revs:
247 raise util.Abort(_('use only one form to specify the revision'))
247 raise util.Abort(_('use only one form to specify the revision'))
248 revs = opts.get('rev')
248 revs = opts.get('rev')
249
249
250 if opts.get('outgoing'):
250 if opts.get('outgoing'):
251 revs = outgoing(dest, opts.get('rev'))
251 revs = outgoing(dest, opts.get('rev'))
252 if opts.get('bundle'):
252 if opts.get('bundle'):
253 opts['revs'] = revs
253 opts['revs'] = revs
254
254
255 # start
255 # start
256 if opts.get('date'):
256 if opts.get('date'):
257 start_time = util.parsedate(opts['date'])
257 start_time = util.parsedate(opts['date'])
258 else:
258 else:
259 start_time = util.makedate()
259 start_time = util.makedate()
260
260
261 def genmsgid(id):
261 def genmsgid(id):
262 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())
263
263
264 def getexportmsgs():
264 def getexportmsgs():
265 patches = []
265 patches = []
266
266
267 class exportee:
267 class exportee:
268 def __init__(self, container):
268 def __init__(self, container):
269 self.lines = []
269 self.lines = []
270 self.container = container
270 self.container = container
271 self.name = 'email'
271 self.name = 'email'
272
272
273 def write(self, data):
273 def write(self, data):
274 self.lines.append(data)
274 self.lines.append(data)
275
275
276 def close(self):
276 def close(self):
277 self.container.append(''.join(self.lines).split('\n'))
277 self.container.append(''.join(self.lines).split('\n'))
278 self.lines = []
278 self.lines = []
279
279
280 commands.export(ui, repo, *revs, **{'output': exportee(patches),
280 commands.export(ui, repo, *revs, **{'output': exportee(patches),
281 'switch_parent': False,
281 'switch_parent': False,
282 'text': None,
282 'text': None,
283 'git': opts.get('git')})
283 'git': opts.get('git')})
284
284
285 jumbo = []
285 jumbo = []
286 msgs = []
286 msgs = []
287
287
288 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))
289
289
290 for p, i in zip(patches, xrange(len(patches))):
290 for p, i in zip(patches, xrange(len(patches))):
291 jumbo.extend(p)
291 jumbo.extend(p)
292 msgs.append(makepatch(p, i + 1, len(patches)))
292 msgs.append(makepatch(p, i + 1, len(patches)))
293
293
294 if len(patches) > 1:
294 if len(patches) > 1:
295 tlen = len(str(len(patches)))
295 tlen = len(str(len(patches)))
296
296
297 subj = '[PATCH %0*d of %d] %s' % (
297 subj = '[PATCH %0*d of %d] %s' % (
298 tlen, 0,
298 tlen, 0,
299 len(patches),
299 len(patches),
300 opts['subject'] or
300 opts['subject'] or
301 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
301 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
302 len(patches))))
302 len(patches))))
303
303
304 body = ''
304 body = ''
305 if opts['diffstat']:
305 if opts['diffstat']:
306 d = cdiffstat(_('Final summary:\n'), jumbo)
306 d = cdiffstat(_('Final summary:\n'), jumbo)
307 if d: body = '\n' + d
307 if d: body = '\n' + d
308
308
309 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'))
310 body = ui.edit(body, sender)
310 body = ui.edit(body, sender)
311
311
312 msg = email.MIMEText.MIMEText(body)
312 msg = email.MIMEText.MIMEText(body)
313 msg['Subject'] = subj
313 msg['Subject'] = subj
314
314
315 msgs.insert(0, msg)
315 msgs.insert(0, msg)
316 return msgs
316 return msgs
317
317
318 def getbundlemsgs(bundle):
318 def getbundlemsgs(bundle):
319 subj = opts['subject'] or \
319 subj = opts['subject'] or \
320 prompt('Subject:', default='A bundle for your repository')
320 prompt('Subject:', default='A bundle for your repository')
321 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
321 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
322 body = ui.edit('', sender)
322 body = ui.edit('', sender)
323
323
324 msg = email.MIMEMultipart.MIMEMultipart()
324 msg = email.MIMEMultipart.MIMEMultipart()
325 if body:
325 if body:
326 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
326 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
327 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
327 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
328 datapart.set_payload(bundle)
328 datapart.set_payload(bundle)
329 datapart.add_header('Content-Disposition', 'attachment',
329 datapart.add_header('Content-Disposition', 'attachment',
330 filename='bundle.hg')
330 filename='bundle.hg')
331 email.Encoders.encode_base64(datapart)
331 email.Encoders.encode_base64(datapart)
332 msg.attach(datapart)
332 msg.attach(datapart)
333 msg['Subject'] = subj
333 msg['Subject'] = subj
334 return [msg]
334 return [msg]
335
335
336 sender = (opts['from'] or ui.config('email', 'from') or
336 sender = (opts['from'] or ui.config('email', 'from') or
337 ui.config('patchbomb', 'from') or
337 ui.config('patchbomb', 'from') or
338 prompt('From', ui.username()))
338 prompt('From', ui.username()))
339
339
340 if opts.get('bundle'):
340 if opts.get('bundle'):
341 msgs = getbundlemsgs(getbundle(dest))
341 msgs = getbundlemsgs(getbundle(dest))
342 else:
342 else:
343 msgs = getexportmsgs()
343 msgs = getexportmsgs()
344
344
345 def getaddrs(opt, prpt, default = None):
345 def getaddrs(opt, prpt, default = None):
346 addrs = opts[opt] or (ui.config('email', opt) or
346 addrs = opts[opt] or (ui.config('email', opt) or
347 ui.config('patchbomb', opt) or
347 ui.config('patchbomb', opt) or
348 prompt(prpt, default = default)).split(',')
348 prompt(prpt, default = default)).split(',')
349 return [a.strip() for a in addrs if a.strip()]
349 return [a.strip() for a in addrs if a.strip()]
350
350
351 to = getaddrs('to', 'To')
351 to = getaddrs('to', 'To')
352 cc = getaddrs('cc', 'Cc', '')
352 cc = getaddrs('cc', 'Cc', '')
353
353
354 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
354 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
355 ui.config('patchbomb', 'bcc') or '').split(',')
355 ui.config('patchbomb', 'bcc') or '').split(',')
356 bcc = [a.strip() for a in bcc if a.strip()]
356 bcc = [a.strip() for a in bcc if a.strip()]
357
357
358 ui.write('\n')
358 ui.write('\n')
359
359
360 if really_sending:
360 if really_sending:
361 mailer = mail.connect(ui)
361 mailer = mail.connect(ui)
362 parent = None
362 parent = None
363
363
364 sender_addr = email.Utils.parseaddr(sender)[1]
364 sender_addr = email.Utils.parseaddr(sender)[1]
365 for m in msgs:
365 for m in msgs:
366 try:
366 try:
367 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
367 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
368 except TypeError:
368 except TypeError:
369 m['Message-Id'] = genmsgid('patchbomb')
369 m['Message-Id'] = genmsgid('patchbomb')
370 if parent:
370 if parent:
371 m['In-Reply-To'] = parent
371 m['In-Reply-To'] = parent
372 else:
372 else:
373 parent = m['Message-Id']
373 parent = m['Message-Id']
374 m['Date'] = util.datestr(date=start_time,
374 m['Date'] = util.datestr(date=start_time,
375 format="%a, %d %b %Y %H:%M:%S", timezone=True)
375 format="%a, %d %b %Y %H:%M:%S", timezone=True)
376
376
377 start_time = (start_time[0] + 1, start_time[1])
377 start_time = (start_time[0] + 1, start_time[1])
378 m['From'] = sender
378 m['From'] = sender
379 m['To'] = ', '.join(to)
379 m['To'] = ', '.join(to)
380 if cc: m['Cc'] = ', '.join(cc)
380 if cc: m['Cc'] = ', '.join(cc)
381 if bcc: m['Bcc'] = ', '.join(bcc)
381 if bcc: m['Bcc'] = ', '.join(bcc)
382 if opts['test']:
382 if opts['test']:
383 ui.status('Displaying ', m['Subject'], ' ...\n')
383 ui.status('Displaying ', m['Subject'], ' ...\n')
384 ui.flush()
384 ui.flush()
385 if 'PAGER' in os.environ:
385 if 'PAGER' in os.environ:
386 fp = os.popen(os['PAGER'], 'w')
386 fp = os.popen(os.environ['PAGER'], 'w')
387 else:
387 else:
388 fp = ui
388 fp = ui
389 try:
389 try:
390 fp.write(m.as_string(0))
390 fp.write(m.as_string(0))
391 fp.write('\n')
391 fp.write('\n')
392 except IOError, inst:
392 except IOError, inst:
393 if inst.errno != errno.EPIPE:
393 if inst.errno != errno.EPIPE:
394 raise
394 raise
395 if fp is not ui:
395 if fp is not ui:
396 fp.close()
396 fp.close()
397 elif opts['mbox']:
397 elif opts['mbox']:
398 ui.status('Writing ', m['Subject'], ' ...\n')
398 ui.status('Writing ', m['Subject'], ' ...\n')
399 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+')
400 date = util.datestr(date=start_time,
400 date = util.datestr(date=start_time,
401 format='%a %b %d %H:%M:%S %Y', timezone=False)
401 format='%a %b %d %H:%M:%S %Y', timezone=False)
402 fp.write('From %s %s\n' % (sender_addr, date))
402 fp.write('From %s %s\n' % (sender_addr, date))
403 fp.write(m.as_string(0))
403 fp.write(m.as_string(0))
404 fp.write('\n\n')
404 fp.write('\n\n')
405 fp.close()
405 fp.close()
406 else:
406 else:
407 ui.status('Sending ', m['Subject'], ' ...\n')
407 ui.status('Sending ', m['Subject'], ' ...\n')
408 # Exim does not remove the Bcc field
408 # Exim does not remove the Bcc field
409 del m['Bcc']
409 del m['Bcc']
410 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
410 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
411
411
412 cmdtable = {
412 cmdtable = {
413 'email':
413 'email':
414 (patchbomb,
414 (patchbomb,
415 [('a', 'attach', None, 'send patches as inline attachments'),
415 [('a', 'attach', None, 'send patches as inline attachments'),
416 ('', 'bcc', [], 'email addresses of blind copy recipients'),
416 ('', 'bcc', [], 'email addresses of blind copy recipients'),
417 ('c', 'cc', [], 'email addresses of copy recipients'),
417 ('c', 'cc', [], 'email addresses of copy recipients'),
418 ('d', 'diffstat', None, 'add diffstat output to messages'),
418 ('d', 'diffstat', None, 'add diffstat output to messages'),
419 ('', 'date', '', _('use the given date as the sending date')),
419 ('', 'date', '', _('use the given date as the sending date')),
420 ('g', 'git', None, _('use git extended diff format')),
420 ('g', 'git', None, _('use git extended diff format')),
421 ('f', 'from', '', 'email address of sender'),
421 ('f', 'from', '', 'email address of sender'),
422 ('', 'plain', None, 'omit hg patch header'),
422 ('', 'plain', None, 'omit hg patch header'),
423 ('n', 'test', None, 'print messages that would be sent'),
423 ('n', 'test', None, 'print messages that would be sent'),
424 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
424 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
425 ('o', 'outgoing', None, _('send changes not found in the target repository')),
425 ('o', 'outgoing', None, _('send changes not found in the target repository')),
426 ('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')),
427 ('r', 'rev', [], _('a revision to send')),
427 ('r', 'rev', [], _('a revision to send')),
428 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
428 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
429 ('t', 'to', [], 'email addresses of recipients'),
429 ('t', 'to', [], 'email addresses of recipients'),
430 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
430 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
431 ('', 'base', [],
431 ('', 'base', [],
432 _('a base changeset to specify instead of a destination (with -b)'))]
432 _('a base changeset to specify instead of a destination (with -b)'))]
433 + commands.remoteopts,
433 + commands.remoteopts,
434 "hg email [OPTION]... [DEST]...")
434 "hg email [OPTION]... [DEST]...")
435 }
435 }
General Comments 0
You need to be logged in to leave comments. Login now