##// END OF EJS Templates
patchbomb: make --bundle respect --desc
Patrick Mezard -
r5753:ea1016b3 default
parent child Browse files
Show More
@@ -1,447 +1,449
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, 'rb').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 getdescription(body, sender):
269 if opts['desc']:
270 body = open(opts['desc']).read()
271 else:
272 ui.write(_('\nWrite the introductory message for the '
273 'patch series.\n\n'))
274 body = ui.edit(body, sender)
275 return body
276
268 def getexportmsgs():
277 def getexportmsgs():
269 patches = []
278 patches = []
270
279
271 class exportee:
280 class exportee:
272 def __init__(self, container):
281 def __init__(self, container):
273 self.lines = []
282 self.lines = []
274 self.container = container
283 self.container = container
275 self.name = 'email'
284 self.name = 'email'
276
285
277 def write(self, data):
286 def write(self, data):
278 self.lines.append(data)
287 self.lines.append(data)
279
288
280 def close(self):
289 def close(self):
281 self.container.append(''.join(self.lines).split('\n'))
290 self.container.append(''.join(self.lines).split('\n'))
282 self.lines = []
291 self.lines = []
283
292
284 commands.export(ui, repo, *revs, **{'output': exportee(patches),
293 commands.export(ui, repo, *revs, **{'output': exportee(patches),
285 'switch_parent': False,
294 'switch_parent': False,
286 'text': None,
295 'text': None,
287 'git': opts.get('git')})
296 'git': opts.get('git')})
288
297
289 jumbo = []
298 jumbo = []
290 msgs = []
299 msgs = []
291
300
292 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
301 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
293
302
294 for p, i in zip(patches, xrange(len(patches))):
303 for p, i in zip(patches, xrange(len(patches))):
295 jumbo.extend(p)
304 jumbo.extend(p)
296 msgs.append(makepatch(p, i + 1, len(patches)))
305 msgs.append(makepatch(p, i + 1, len(patches)))
297
306
298 if len(patches) > 1:
307 if len(patches) > 1:
299 tlen = len(str(len(patches)))
308 tlen = len(str(len(patches)))
300
309
301 subj = '[PATCH %0*d of %d] %s' % (
310 subj = '[PATCH %0*d of %d] %s' % (
302 tlen, 0,
311 tlen, 0,
303 len(patches),
312 len(patches),
304 opts['subject'] or
313 opts['subject'] or
305 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
314 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
306 len(patches))))
315 len(patches))))
307
316
308 body = ''
317 body = ''
309 if opts['diffstat']:
318 if opts['diffstat']:
310 d = cdiffstat(_('Final summary:\n'), jumbo)
319 d = cdiffstat(_('Final summary:\n'), jumbo)
311 if d: body = '\n' + d
320 if d: body = '\n' + d
312
321
313 if opts['desc']:
322 body = getdescription(body, sender)
314 body = open(opts['desc']).read()
315 else:
316 ui.write(_('\nWrite the introductory message for the '
317 'patch series.\n\n'))
318 body = ui.edit(body, sender)
319
320 msg = email.MIMEText.MIMEText(body)
323 msg = email.MIMEText.MIMEText(body)
321 msg['Subject'] = subj
324 msg['Subject'] = subj
322
325
323 msgs.insert(0, msg)
326 msgs.insert(0, msg)
324 return msgs
327 return msgs
325
328
326 def getbundlemsgs(bundle):
329 def getbundlemsgs(bundle):
327 subj = (opts['subject']
330 subj = (opts['subject']
328 or prompt('Subject:', default='A bundle for your repository'))
331 or prompt('Subject:', default='A bundle for your repository'))
329 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
330 body = ui.edit('', sender)
331
332
333 body = getdescription('', sender)
332 msg = email.MIMEMultipart.MIMEMultipart()
334 msg = email.MIMEMultipart.MIMEMultipart()
333 if body:
335 if body:
334 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
336 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
335 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
337 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
336 datapart.set_payload(bundle)
338 datapart.set_payload(bundle)
337 datapart.add_header('Content-Disposition', 'attachment',
339 datapart.add_header('Content-Disposition', 'attachment',
338 filename='bundle.hg')
340 filename='bundle.hg')
339 email.Encoders.encode_base64(datapart)
341 email.Encoders.encode_base64(datapart)
340 msg.attach(datapart)
342 msg.attach(datapart)
341 msg['Subject'] = subj
343 msg['Subject'] = subj
342 return [msg]
344 return [msg]
343
345
344 sender = (opts['from'] or ui.config('email', 'from') or
346 sender = (opts['from'] or ui.config('email', 'from') or
345 ui.config('patchbomb', 'from') or
347 ui.config('patchbomb', 'from') or
346 prompt('From', ui.username()))
348 prompt('From', ui.username()))
347
349
348 if opts.get('bundle'):
350 if opts.get('bundle'):
349 msgs = getbundlemsgs(getbundle(dest))
351 msgs = getbundlemsgs(getbundle(dest))
350 else:
352 else:
351 msgs = getexportmsgs()
353 msgs = getexportmsgs()
352
354
353 def getaddrs(opt, prpt, default = None):
355 def getaddrs(opt, prpt, default = None):
354 addrs = opts[opt] or (ui.config('email', opt) or
356 addrs = opts[opt] or (ui.config('email', opt) or
355 ui.config('patchbomb', opt) or
357 ui.config('patchbomb', opt) or
356 prompt(prpt, default = default)).split(',')
358 prompt(prpt, default = default)).split(',')
357 return [a.strip() for a in addrs if a.strip()]
359 return [a.strip() for a in addrs if a.strip()]
358
360
359 to = getaddrs('to', 'To')
361 to = getaddrs('to', 'To')
360 cc = getaddrs('cc', 'Cc', '')
362 cc = getaddrs('cc', 'Cc', '')
361
363
362 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
364 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
363 ui.config('patchbomb', 'bcc') or '').split(',')
365 ui.config('patchbomb', 'bcc') or '').split(',')
364 bcc = [a.strip() for a in bcc if a.strip()]
366 bcc = [a.strip() for a in bcc if a.strip()]
365
367
366 ui.write('\n')
368 ui.write('\n')
367
369
368 parent = None
370 parent = None
369
371
370 sender_addr = email.Utils.parseaddr(sender)[1]
372 sender_addr = email.Utils.parseaddr(sender)[1]
371 for m in msgs:
373 for m in msgs:
372 try:
374 try:
373 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
375 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
374 except TypeError:
376 except TypeError:
375 m['Message-Id'] = genmsgid('patchbomb')
377 m['Message-Id'] = genmsgid('patchbomb')
376 if parent:
378 if parent:
377 m['In-Reply-To'] = parent
379 m['In-Reply-To'] = parent
378 else:
380 else:
379 parent = m['Message-Id']
381 parent = m['Message-Id']
380 m['Date'] = util.datestr(date=start_time,
382 m['Date'] = util.datestr(date=start_time,
381 format="%a, %d %b %Y %H:%M:%S", timezone=True)
383 format="%a, %d %b %Y %H:%M:%S", timezone=True)
382
384
383 start_time = (start_time[0] + 1, start_time[1])
385 start_time = (start_time[0] + 1, start_time[1])
384 m['From'] = sender
386 m['From'] = sender
385 m['To'] = ', '.join(to)
387 m['To'] = ', '.join(to)
386 if cc: m['Cc'] = ', '.join(cc)
388 if cc: m['Cc'] = ', '.join(cc)
387 if bcc: m['Bcc'] = ', '.join(bcc)
389 if bcc: m['Bcc'] = ', '.join(bcc)
388 if opts['test']:
390 if opts['test']:
389 ui.status('Displaying ', m['Subject'], ' ...\n')
391 ui.status('Displaying ', m['Subject'], ' ...\n')
390 ui.flush()
392 ui.flush()
391 if 'PAGER' in os.environ:
393 if 'PAGER' in os.environ:
392 fp = os.popen(os.environ['PAGER'], 'w')
394 fp = os.popen(os.environ['PAGER'], 'w')
393 else:
395 else:
394 fp = ui
396 fp = ui
395 try:
397 try:
396 fp.write(m.as_string(0))
398 fp.write(m.as_string(0))
397 fp.write('\n')
399 fp.write('\n')
398 except IOError, inst:
400 except IOError, inst:
399 if inst.errno != errno.EPIPE:
401 if inst.errno != errno.EPIPE:
400 raise
402 raise
401 if fp is not ui:
403 if fp is not ui:
402 fp.close()
404 fp.close()
403 elif opts['mbox']:
405 elif opts['mbox']:
404 ui.status('Writing ', m['Subject'], ' ...\n')
406 ui.status('Writing ', m['Subject'], ' ...\n')
405 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
407 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
406 date = util.datestr(date=start_time,
408 date = util.datestr(date=start_time,
407 format='%a %b %d %H:%M:%S %Y', timezone=False)
409 format='%a %b %d %H:%M:%S %Y', timezone=False)
408 fp.write('From %s %s\n' % (sender_addr, date))
410 fp.write('From %s %s\n' % (sender_addr, date))
409 fp.write(m.as_string(0))
411 fp.write(m.as_string(0))
410 fp.write('\n\n')
412 fp.write('\n\n')
411 fp.close()
413 fp.close()
412 else:
414 else:
413 ui.status('Sending ', m['Subject'], ' ...\n')
415 ui.status('Sending ', m['Subject'], ' ...\n')
414 # Exim does not remove the Bcc field
416 # Exim does not remove the Bcc field
415 del m['Bcc']
417 del m['Bcc']
416 mail.sendmail(ui, sender, to + bcc + cc, m.as_string(0))
418 mail.sendmail(ui, sender, to + bcc + cc, m.as_string(0))
417
419
418 cmdtable = {
420 cmdtable = {
419 "email":
421 "email":
420 (patchbomb,
422 (patchbomb,
421 [('a', 'attach', None, _('send patches as inline attachments')),
423 [('a', 'attach', None, _('send patches as inline attachments')),
422 ('', 'bcc', [], _('email addresses of blind copy recipients')),
424 ('', 'bcc', [], _('email addresses of blind copy recipients')),
423 ('c', 'cc', [], _('email addresses of copy recipients')),
425 ('c', 'cc', [], _('email addresses of copy recipients')),
424 ('d', 'diffstat', None, _('add diffstat output to messages')),
426 ('d', 'diffstat', None, _('add diffstat output to messages')),
425 ('', 'date', '', _('use the given date as the sending date')),
427 ('', 'date', '', _('use the given date as the sending date')),
426 ('', 'desc', '', _('use the given file as the series description')),
428 ('', 'desc', '', _('use the given file as the series description')),
427 ('g', 'git', None, _('use git extended diff format')),
429 ('g', 'git', None, _('use git extended diff format')),
428 ('f', 'from', '', _('email address of sender')),
430 ('f', 'from', '', _('email address of sender')),
429 ('', 'plain', None, _('omit hg patch header')),
431 ('', 'plain', None, _('omit hg patch header')),
430 ('n', 'test', None, _('print messages that would be sent')),
432 ('n', 'test', None, _('print messages that would be sent')),
431 ('m', 'mbox', '',
433 ('m', 'mbox', '',
432 _('write messages to mbox file instead of sending them')),
434 _('write messages to mbox file instead of sending them')),
433 ('o', 'outgoing', None,
435 ('o', 'outgoing', None,
434 _('send changes not found in the target repository')),
436 _('send changes not found in the target repository')),
435 ('b', 'bundle', None,
437 ('b', 'bundle', None,
436 _('send changes not in target as a binary bundle')),
438 _('send changes not in target as a binary bundle')),
437 ('r', 'rev', [], _('a revision to send')),
439 ('r', 'rev', [], _('a revision to send')),
438 ('s', 'subject', '',
440 ('s', 'subject', '',
439 _('subject of first message (intro or single patch)')),
441 _('subject of first message (intro or single patch)')),
440 ('t', 'to', [], _('email addresses of recipients')),
442 ('t', 'to', [], _('email addresses of recipients')),
441 ('', 'force', None,
443 ('', 'force', None,
442 _('run even when remote repository is unrelated (with -b)')),
444 _('run even when remote repository is unrelated (with -b)')),
443 ('', 'base', [],
445 ('', 'base', [],
444 _('a base changeset to specify instead of a destination (with -b)')),
446 _('a base changeset to specify instead of a destination (with -b)')),
445 ] + commands.remoteopts,
447 ] + commands.remoteopts,
446 _('hg email [OPTION]... [DEST]...'))
448 _('hg email [OPTION]... [DEST]...'))
447 }
449 }
@@ -1,20 +1,46
1 #!/bin/sh
1 #!/bin/sh
2
2
3 fixheaders()
4 {
5 sed -e 's/\(Message-Id:.*@\).*/\1/' \
6 -e 's/\(In-Reply-To:.*@\).*/\1/' \
7 -e 's/===.*/===/'
8 }
9
3 echo "[extensions]" >> $HGRCPATH
10 echo "[extensions]" >> $HGRCPATH
4 echo "patchbomb=" >> $HGRCPATH
11 echo "patchbomb=" >> $HGRCPATH
5
12
6 hg init
13 hg init t
14 cd t
7 echo a > a
15 echo a > a
8 hg commit -Ama -d '1 0'
16 hg commit -Ama -d '1 0'
9
17
10 hg email --date '1970-1-1 0:1' -n -f quux -t foo -c bar tip | \
18 hg email --date '1970-1-1 0:1' -n -f quux -t foo -c bar tip | \
11 sed -e 's/\(Message-Id:.*@\).*/\1/'
19 fixheaders
12
20
13 echo b > b
21 echo b > b
14 hg commit -Amb -d '2 0'
22 hg commit -Amb -d '2 0'
15
23
16 hg email --date '1970-1-1 0:2' -n -f quux -t foo -c bar -s test 0:tip | \
24 hg email --date '1970-1-1 0:2' -n -f quux -t foo -c bar -s test 0:tip | \
17 sed -e 's/\(Message-Id:.*@\).*/\1/' | \
25 fixheaders
18 sed -e 's/\(In-Reply-To:.*@\).*/\1/'
19
26
20 hg email -m test.mbox -f quux -t foo -c bar -s test 0:tip
27 hg email -m test.mbox -f quux -t foo -c bar -s test 0:tip
28
29 cd ..
30
31 hg clone -q t t2
32 cd t2
33 echo c > c
34 hg commit -Amc -d '3 0'
35
36 cat > description <<EOF
37 a multiline
38
39 description
40 EOF
41
42 echo % test bundle and description
43 hg email --date '1970-1-1 0:3' -n -f quux -t foo \
44 -c bar -s test -r tip -b --desc description | \
45 fixheaders
46
@@ -1,109 +1,146
1 adding a
1 adding a
2 This patch series consists of 1 patches.
2 This patch series consists of 1 patches.
3
3
4
4
5 Displaying [PATCH] a ...
5 Displaying [PATCH] a ...
6 Content-Type: text/plain; charset="us-ascii"
6 Content-Type: text/plain; charset="us-ascii"
7 MIME-Version: 1.0
7 MIME-Version: 1.0
8 Content-Transfer-Encoding: 7bit
8 Content-Transfer-Encoding: 7bit
9 Subject: [PATCH] a
9 Subject: [PATCH] a
10 X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
10 X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
11 Message-Id: <8580ff50825a50c8f716.60@
11 Message-Id: <8580ff50825a50c8f716.60@
12 Date: Thu, 01 Jan 1970 00:01:00 +0000
12 Date: Thu, 01 Jan 1970 00:01:00 +0000
13 From: quux
13 From: quux
14 To: foo
14 To: foo
15 Cc: bar
15 Cc: bar
16
16
17 # HG changeset patch
17 # HG changeset patch
18 # User test
18 # User test
19 # Date 1 0
19 # Date 1 0
20 # Node ID 8580ff50825a50c8f716709acdf8de0deddcd6ab
20 # Node ID 8580ff50825a50c8f716709acdf8de0deddcd6ab
21 # Parent 0000000000000000000000000000000000000000
21 # Parent 0000000000000000000000000000000000000000
22 a
22 a
23
23
24 diff -r 000000000000 -r 8580ff50825a a
24 diff -r 000000000000 -r 8580ff50825a a
25 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
25 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
26 +++ b/a Thu Jan 01 00:00:01 1970 +0000
26 +++ b/a Thu Jan 01 00:00:01 1970 +0000
27 @@ -0,0 +1,1 @@
27 @@ -0,0 +1,1 @@
28 +a
28 +a
29
29
30 adding b
30 adding b
31 This patch series consists of 2 patches.
31 This patch series consists of 2 patches.
32
32
33
33
34 Write the introductory message for the patch series.
34 Write the introductory message for the patch series.
35
35
36
36
37 Displaying [PATCH 0 of 2] test ...
37 Displaying [PATCH 0 of 2] test ...
38 Content-Type: text/plain; charset="us-ascii"
38 Content-Type: text/plain; charset="us-ascii"
39 MIME-Version: 1.0
39 MIME-Version: 1.0
40 Content-Transfer-Encoding: 7bit
40 Content-Transfer-Encoding: 7bit
41 Subject: [PATCH 0 of 2] test
41 Subject: [PATCH 0 of 2] test
42 Message-Id: <patchbomb.120@
42 Message-Id: <patchbomb.120@
43 Date: Thu, 01 Jan 1970 00:02:00 +0000
43 Date: Thu, 01 Jan 1970 00:02:00 +0000
44 From: quux
44 From: quux
45 To: foo
45 To: foo
46 Cc: bar
46 Cc: bar
47
47
48
48
49 Displaying [PATCH 1 of 2] a ...
49 Displaying [PATCH 1 of 2] a ...
50 Content-Type: text/plain; charset="us-ascii"
50 Content-Type: text/plain; charset="us-ascii"
51 MIME-Version: 1.0
51 MIME-Version: 1.0
52 Content-Transfer-Encoding: 7bit
52 Content-Transfer-Encoding: 7bit
53 Subject: [PATCH 1 of 2] a
53 Subject: [PATCH 1 of 2] a
54 X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
54 X-Mercurial-Node: 8580ff50825a50c8f716709acdf8de0deddcd6ab
55 Message-Id: <8580ff50825a50c8f716.121@
55 Message-Id: <8580ff50825a50c8f716.121@
56 In-Reply-To: <patchbomb.120@
56 In-Reply-To: <patchbomb.120@
57 Date: Thu, 01 Jan 1970 00:02:01 +0000
57 Date: Thu, 01 Jan 1970 00:02:01 +0000
58 From: quux
58 From: quux
59 To: foo
59 To: foo
60 Cc: bar
60 Cc: bar
61
61
62 # HG changeset patch
62 # HG changeset patch
63 # User test
63 # User test
64 # Date 1 0
64 # Date 1 0
65 # Node ID 8580ff50825a50c8f716709acdf8de0deddcd6ab
65 # Node ID 8580ff50825a50c8f716709acdf8de0deddcd6ab
66 # Parent 0000000000000000000000000000000000000000
66 # Parent 0000000000000000000000000000000000000000
67 a
67 a
68
68
69 diff -r 000000000000 -r 8580ff50825a a
69 diff -r 000000000000 -r 8580ff50825a a
70 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
70 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
71 +++ b/a Thu Jan 01 00:00:01 1970 +0000
71 +++ b/a Thu Jan 01 00:00:01 1970 +0000
72 @@ -0,0 +1,1 @@
72 @@ -0,0 +1,1 @@
73 +a
73 +a
74
74
75 Displaying [PATCH 2 of 2] b ...
75 Displaying [PATCH 2 of 2] b ...
76 Content-Type: text/plain; charset="us-ascii"
76 Content-Type: text/plain; charset="us-ascii"
77 MIME-Version: 1.0
77 MIME-Version: 1.0
78 Content-Transfer-Encoding: 7bit
78 Content-Transfer-Encoding: 7bit
79 Subject: [PATCH 2 of 2] b
79 Subject: [PATCH 2 of 2] b
80 X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
80 X-Mercurial-Node: 97d72e5f12c7e84f85064aa72e5a297142c36ed9
81 Message-Id: <97d72e5f12c7e84f8506.122@
81 Message-Id: <97d72e5f12c7e84f8506.122@
82 In-Reply-To: <patchbomb.120@
82 In-Reply-To: <patchbomb.120@
83 Date: Thu, 01 Jan 1970 00:02:02 +0000
83 Date: Thu, 01 Jan 1970 00:02:02 +0000
84 From: quux
84 From: quux
85 To: foo
85 To: foo
86 Cc: bar
86 Cc: bar
87
87
88 # HG changeset patch
88 # HG changeset patch
89 # User test
89 # User test
90 # Date 2 0
90 # Date 2 0
91 # Node ID 97d72e5f12c7e84f85064aa72e5a297142c36ed9
91 # Node ID 97d72e5f12c7e84f85064aa72e5a297142c36ed9
92 # Parent 8580ff50825a50c8f716709acdf8de0deddcd6ab
92 # Parent 8580ff50825a50c8f716709acdf8de0deddcd6ab
93 b
93 b
94
94
95 diff -r 8580ff50825a -r 97d72e5f12c7 b
95 diff -r 8580ff50825a -r 97d72e5f12c7 b
96 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
96 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
97 +++ b/b Thu Jan 01 00:00:02 1970 +0000
97 +++ b/b Thu Jan 01 00:00:02 1970 +0000
98 @@ -0,0 +1,1 @@
98 @@ -0,0 +1,1 @@
99 +b
99 +b
100
100
101 This patch series consists of 2 patches.
101 This patch series consists of 2 patches.
102
102
103
103
104 Write the introductory message for the patch series.
104 Write the introductory message for the patch series.
105
105
106
106
107 Writing [PATCH 0 of 2] test ...
107 Writing [PATCH 0 of 2] test ...
108 Writing [PATCH 1 of 2] a ...
108 Writing [PATCH 1 of 2] a ...
109 Writing [PATCH 2 of 2] b ...
109 Writing [PATCH 2 of 2] b ...
110 adding c
111 % test bundle and description
112 searching for changes
113
114 Displaying test ...
115 Content-Type: multipart/mixed; boundary="===
116 MIME-Version: 1.0
117 Subject: test
118 Message-Id: <patchbomb.180@
119 Date: Thu, 01 Jan 1970 00:03:00 +0000
120 From: quux
121 To: foo
122 Cc: bar
123
124 --===
125 Content-Type: text/plain; charset="us-ascii"
126 MIME-Version: 1.0
127 Content-Transfer-Encoding: 7bit
128
129 a multiline
130
131 description
132
133 --===
134 Content-Type: application/x-mercurial-bundle
135 MIME-Version: 1.0
136 Content-Disposition: attachment; filename="bundle.hg"
137 Content-Transfer-Encoding: base64
138
139 SEcxMEJaaDkxQVkmU1nvR7I3AAAN////lFYQWj1/4HwRkdC/AywIAk0E4pfoSIIIgQCgGEQOcLAA
140 2tA1VPyp4mkeoG0EaaPU0GTT1GjRiNPIg9CZGBqZ6UbU9J+KFU09DNUaGgAAAAAANAGgAAAAA1U8
141 oGgAADQGgAANNANAAAAAAZipFLz3XoakCEQB3PVPyHJVi1iYkAAKQAZQGpQGZESInRnCFMqLDla2
142 Bx3qfRQeA2N4lnzKkAmP8kR2asievLLXXebVU8Vg4iEBqcJNJAxIapSU6SM4888ZAciRG6MYAIEE
143 SlIBpFisgGkyRjX//TMtfcUAEsGu56+YnE1OlTZmzKm8BSu2rvo4rHAYYaadIFFuTy0LYgIkgLVD
144 sgVa2F19D1tx9+hgbAygLgQwaIqcDdgA4BjQgIiz/AEP72++llgDKhKducqodGE4B0ETqF3JFOFC
145 Q70eyNw=
146 --===
General Comments 0
You need to be logged in to leave comments. Login now