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