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