##// END OF EJS Templates
patchbomb: attachment options changed...
Dennis Schoen -
r5819:89ea99c7 default
parent child Browse files
Show More
@@ -1,459 +1,464 b''
1 1 # Command for sending a collection of Mercurial changesets as a series
2 2 # of patch emails.
3 3 #
4 4 # The series is started off with a "[PATCH 0 of N]" introduction,
5 5 # which describes the series as a whole.
6 6 #
7 7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
8 8 # the first line of the changeset description as the subject text.
9 9 # The message contains two or three body parts:
10 10 #
11 11 # The remainder of the changeset description.
12 12 #
13 13 # [Optional] If the diffstat program is installed, the result of
14 14 # running diffstat on the patch.
15 15 #
16 16 # The patch itself, as generated by "hg export".
17 17 #
18 18 # Each message refers to all of its predecessors using the In-Reply-To
19 19 # and References headers, so they will show up as a sequence in
20 20 # threaded mail and news readers, and in mail archives.
21 21 #
22 22 # For each changeset, you will be prompted with a diffstat summary and
23 23 # the changeset summary, so you can be sure you are sending the right
24 24 # changes.
25 25 #
26 26 # To enable this extension:
27 27 #
28 28 # [extensions]
29 29 # hgext.patchbomb =
30 30 #
31 31 # To configure other defaults, add a section like this to your hgrc
32 32 # file:
33 33 #
34 34 # [email]
35 35 # from = My Name <my@email>
36 36 # to = recipient1, recipient2, ...
37 37 # cc = cc1, cc2, ...
38 38 # bcc = bcc1, bcc2, ...
39 39 #
40 40 # Then you can use the "hg email" command to mail a series of changesets
41 41 # as a patchbomb.
42 42 #
43 43 # To avoid sending patches prematurely, it is a good idea to first run
44 44 # the "email" command with the "-n" option (test only). You will be
45 45 # prompted for an email recipient address, a subject an an introductory
46 46 # message describing the patches of your patchbomb. Then when all is
47 47 # done, patchbomb messages are displayed. If PAGER environment variable
48 48 # is set, your pager will be fired up once for each patchbomb message, so
49 49 # you can verify everything is alright.
50 50 #
51 51 # The "-m" (mbox) option is also very useful. Instead of previewing
52 52 # each patchbomb message in a pager or sending the messages directly,
53 53 # it will create a UNIX mailbox file with the patch emails. This
54 54 # mailbox file can be previewed with any mail user agent which supports
55 55 # UNIX mbox files, i.e. with mutt:
56 56 #
57 57 # % mutt -R -f mbox
58 58 #
59 59 # When you are previewing the patchbomb messages, you can use `formail'
60 60 # (a utility that is commonly installed as part of the procmail package),
61 61 # to send each message out:
62 62 #
63 63 # % formail -s sendmail -bm -t < mbox
64 64 #
65 65 # That should be all. Now your patchbomb is on its way out.
66 66
67 67 import os, errno, socket, tempfile
68 68 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
69 69 import email.Utils, email.Encoders
70 70 from mercurial import cmdutil, commands, hg, mail, ui, patch, util
71 71 from mercurial.i18n import _
72 72 from mercurial.node import *
73 73
74 74 def patchbomb(ui, repo, *revs, **opts):
75 75 '''send changesets by email
76 76
77 77 By default, diffs are sent in the format generated by hg export,
78 78 one per message. The series starts with a "[PATCH 0 of N]"
79 79 introduction, which describes the series as a whole.
80 80
81 81 Each patch email has a Subject line of "[PATCH M of N] ...", using
82 82 the first line of the changeset description as the subject text.
83 83 The message contains two or three body parts. First, the rest of
84 84 the changeset description. Next, (optionally) if the diffstat
85 85 program is installed, the result of running diffstat on the patch.
86 86 Finally, the patch itself, as generated by "hg export".
87 87
88 88 With --outgoing, emails will be generated for patches not
89 89 found in the destination repository (or only those which are
90 90 ancestors of the specified revisions if any are provided)
91 91
92 92 With --bundle, changesets are selected as for --outgoing,
93 93 but a single email containing a binary Mercurial bundle as an
94 94 attachment will be sent.
95 95
96 96 Examples:
97 97
98 98 hg email -r 3000 # send patch 3000 only
99 99 hg email -r 3000 -r 3001 # send patches 3000 and 3001
100 100 hg email -r 3000:3005 # send patches 3000 through 3005
101 101 hg email 3000 # send patch 3000 (deprecated)
102 102
103 103 hg email -o # send all patches not in default
104 104 hg email -o DEST # send all patches not in DEST
105 105 hg email -o -r 3000 # send all ancestors of 3000 not in default
106 106 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
107 107
108 108 hg email -b # send bundle of all patches not in default
109 109 hg email -b DEST # send bundle of all patches not in DEST
110 110 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
111 111 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
112 112
113 113 Before using this command, you will need to enable email in your hgrc.
114 114 See the [email] section in hgrc(5) for details.
115 115 '''
116 116
117 117 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
118 118 if not ui.interactive:
119 119 return default
120 120 if default:
121 121 prompt += ' [%s]' % default
122 122 prompt += rest
123 123 while True:
124 124 r = ui.prompt(prompt, default=default)
125 125 if r:
126 126 return r
127 127 if default is not None:
128 128 return default
129 129 if empty_ok:
130 130 return r
131 131 ui.warn(_('Please enter a valid value.\n'))
132 132
133 133 def confirm(s, denial):
134 134 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
135 135 raise util.Abort(denial)
136 136
137 137 def cdiffstat(summary, patchlines):
138 138 s = patch.diffstat(patchlines)
139 139 if s:
140 140 if summary:
141 141 ui.write(summary, '\n')
142 142 ui.write(s, '\n')
143 143 confirm(_('Does the diffstat above look okay'),
144 144 _('diffstat rejected'))
145 145 elif s is None:
146 146 ui.warn(_('No diffstat information available.\n'))
147 147 s = ''
148 148 return s
149 149
150 150 def makepatch(patch, idx, total):
151 151 desc = []
152 152 node = None
153 153 body = ''
154 154 for line in patch:
155 155 if line.startswith('#'):
156 156 if line.startswith('# Node ID'):
157 157 node = line.split()[-1]
158 158 continue
159 159 if line.startswith('diff -r') or line.startswith('diff --git'):
160 160 break
161 161 desc.append(line)
162 162 if not node:
163 163 raise ValueError
164 164
165 #body = ('\n'.join(desc[1:]).strip() or
166 # 'Patch subject is complete summary.')
167 #body += '\n\n\n'
165 if opts['attach']:
166 body = ('\n'.join(desc[1:]).strip() or
167 'Patch subject is complete summary.')
168 body += '\n\n\n'
168 169
169 170 if opts.get('plain'):
170 171 while patch and patch[0].startswith('# '):
171 172 patch.pop(0)
172 173 if patch:
173 174 patch.pop(0)
174 175 while patch and not patch[0].strip():
175 176 patch.pop(0)
176 177 if opts.get('diffstat'):
177 178 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
178 if opts.get('attach'):
179 if opts.get('attach') or opts.get('inline'):
179 180 msg = email.MIMEMultipart.MIMEMultipart()
180 181 if body:
181 182 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
182 183 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
183 184 binnode = bin(node)
184 185 # if node is mq patch, it will have patch file name as tag
185 186 patchname = [t for t in repo.nodetags(binnode)
186 187 if t.endswith('.patch') or t.endswith('.diff')]
187 188 if patchname:
188 189 patchname = patchname[0]
189 190 elif total > 1:
190 191 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
191 192 binnode, idx, total)
192 193 else:
193 194 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
194 p['Content-Disposition'] = 'inline; filename=' + patchname
195 disposition = 'inline'
196 if opts['attach']:
197 disposition = 'attachment'
198 p['Content-Disposition'] = disposition + '; filename=' + patchname
195 199 msg.attach(p)
196 200 else:
197 201 body += '\n'.join(patch)
198 202 msg = email.MIMEText.MIMEText(body)
199 203
200 204 subj = desc[0].strip().rstrip('. ')
201 205 if total == 1:
202 206 subj = '[PATCH] ' + (opts.get('subject') or subj)
203 207 else:
204 208 tlen = len(str(total))
205 209 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
206 210 msg['Subject'] = subj
207 211 msg['X-Mercurial-Node'] = node
208 212 return msg
209 213
210 214 def outgoing(dest, revs):
211 215 '''Return the revisions present locally but not in dest'''
212 216 dest = ui.expandpath(dest or 'default-push', dest or 'default')
213 217 revs = [repo.lookup(rev) for rev in revs]
214 218 other = hg.repository(ui, dest)
215 219 ui.status(_('comparing with %s\n') % dest)
216 220 o = repo.findoutgoing(other)
217 221 if not o:
218 222 ui.status(_("no changes found\n"))
219 223 return []
220 224 o = repo.changelog.nodesbetween(o, revs or None)[0]
221 225 return [str(repo.changelog.rev(r)) for r in o]
222 226
223 227 def getbundle(dest):
224 228 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
225 229 tmpfn = os.path.join(tmpdir, 'bundle')
226 230 try:
227 231 commands.bundle(ui, repo, tmpfn, dest, **opts)
228 232 return open(tmpfn, 'rb').read()
229 233 finally:
230 234 try:
231 235 os.unlink(tmpfn)
232 236 except:
233 237 pass
234 238 os.rmdir(tmpdir)
235 239
236 240 if not (opts.get('test') or opts.get('mbox')):
237 241 # really sending
238 242 mail.validateconfig(ui)
239 243
240 244 if not (revs or opts.get('rev')
241 245 or opts.get('outgoing') or opts.get('bundle')):
242 246 raise util.Abort(_('specify at least one changeset with -r or -o'))
243 247
244 248 cmdutil.setremoteconfig(ui, opts)
245 249 if opts.get('outgoing') and opts.get('bundle'):
246 250 raise util.Abort(_("--outgoing mode always on with --bundle;"
247 251 " do not re-specify --outgoing"))
248 252
249 253 if opts.get('outgoing') or opts.get('bundle'):
250 254 if len(revs) > 1:
251 255 raise util.Abort(_("too many destinations"))
252 256 dest = revs and revs[0] or None
253 257 revs = []
254 258
255 259 if opts.get('rev'):
256 260 if revs:
257 261 raise util.Abort(_('use only one form to specify the revision'))
258 262 revs = opts.get('rev')
259 263
260 264 if opts.get('outgoing'):
261 265 revs = outgoing(dest, opts.get('rev'))
262 266 if opts.get('bundle'):
263 267 opts['revs'] = revs
264 268
265 269 # start
266 270 if opts.get('date'):
267 271 start_time = util.parsedate(opts.get('date'))
268 272 else:
269 273 start_time = util.makedate()
270 274
271 275 def genmsgid(id):
272 276 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
273 277
274 278 def getdescription(body, sender):
275 279 if opts.get('desc'):
276 280 body = open(opts.get('desc')).read()
277 281 else:
278 282 ui.write(_('\nWrite the introductory message for the '
279 283 'patch series.\n\n'))
280 284 body = ui.edit(body, sender)
281 285 return body
282 286
283 287 def getexportmsgs():
284 288 patches = []
285 289
286 290 class exportee:
287 291 def __init__(self, container):
288 292 self.lines = []
289 293 self.container = container
290 294 self.name = 'email'
291 295
292 296 def write(self, data):
293 297 self.lines.append(data)
294 298
295 299 def close(self):
296 300 self.container.append(''.join(self.lines).split('\n'))
297 301 self.lines = []
298 302
299 303 commands.export(ui, repo, *revs, **{'output': exportee(patches),
300 304 'switch_parent': False,
301 305 'text': None,
302 306 'git': opts.get('git')})
303 307
304 308 jumbo = []
305 309 msgs = []
306 310
307 311 ui.write(_('This patch series consists of %d patches.\n\n')
308 312 % len(patches))
309 313
310 314 for p, i in zip(patches, xrange(len(patches))):
311 315 jumbo.extend(p)
312 316 msgs.append(makepatch(p, i + 1, len(patches)))
313 317
314 318 if len(patches) > 1:
315 319 tlen = len(str(len(patches)))
316 320
317 321 subj = '[PATCH %0*d of %d] %s' % (
318 322 tlen, 0, len(patches),
319 323 opts.get('subject') or
320 324 prompt('Subject:',
321 325 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
322 326
323 327 body = ''
324 328 if opts.get('diffstat'):
325 329 d = cdiffstat(_('Final summary:\n'), jumbo)
326 330 if d:
327 331 body = '\n' + d
328 332
329 333 body = getdescription(body, sender)
330 334 msg = email.MIMEText.MIMEText(body)
331 335 msg['Subject'] = subj
332 336
333 337 msgs.insert(0, msg)
334 338 return msgs
335 339
336 340 def getbundlemsgs(bundle):
337 341 subj = (opts.get('subject')
338 342 or prompt('Subject:', default='A bundle for your repository'))
339 343
340 344 body = getdescription('', sender)
341 345 msg = email.MIMEMultipart.MIMEMultipart()
342 346 if body:
343 347 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
344 348 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
345 349 datapart.set_payload(bundle)
346 350 datapart.add_header('Content-Disposition', 'attachment',
347 351 filename='bundle.hg')
348 352 email.Encoders.encode_base64(datapart)
349 353 msg.attach(datapart)
350 354 msg['Subject'] = subj
351 355 return [msg]
352 356
353 357 sender = (opts.get('from') or ui.config('email', 'from') or
354 358 ui.config('patchbomb', 'from') or
355 359 prompt('From', ui.username()))
356 360
357 361 if opts.get('bundle'):
358 362 msgs = getbundlemsgs(getbundle(dest))
359 363 else:
360 364 msgs = getexportmsgs()
361 365
362 366 def getaddrs(opt, prpt, default = None):
363 367 addrs = opts.get(opt) or (ui.config('email', opt) or
364 368 ui.config('patchbomb', opt) or
365 369 prompt(prpt, default = default)).split(',')
366 370 return [a.strip() for a in addrs if a.strip()]
367 371
368 372 to = getaddrs('to', 'To')
369 373 cc = getaddrs('cc', 'Cc', '')
370 374
371 375 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
372 376 ui.config('patchbomb', 'bcc') or '').split(',')
373 377 bcc = [a.strip() for a in bcc if a.strip()]
374 378
375 379 ui.write('\n')
376 380
377 381 parent = None
378 382
379 383 sender_addr = email.Utils.parseaddr(sender)[1]
380 384 for m in msgs:
381 385 try:
382 386 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
383 387 except TypeError:
384 388 m['Message-Id'] = genmsgid('patchbomb')
385 389 if parent:
386 390 m['In-Reply-To'] = parent
387 391 else:
388 392 parent = m['Message-Id']
389 393 m['Date'] = util.datestr(date=start_time,
390 394 format="%a, %d %b %Y %H:%M:%S", timezone=True)
391 395
392 396 start_time = (start_time[0] + 1, start_time[1])
393 397 m['From'] = sender
394 398 m['To'] = ', '.join(to)
395 399 if cc:
396 400 m['Cc'] = ', '.join(cc)
397 401 if bcc:
398 402 m['Bcc'] = ', '.join(bcc)
399 403 if opts.get('test'):
400 404 ui.status('Displaying ', m['Subject'], ' ...\n')
401 405 ui.flush()
402 406 if 'PAGER' in os.environ:
403 407 fp = os.popen(os.environ['PAGER'], 'w')
404 408 else:
405 409 fp = ui
406 410 try:
407 411 fp.write(m.as_string(0))
408 412 fp.write('\n')
409 413 except IOError, inst:
410 414 if inst.errno != errno.EPIPE:
411 415 raise
412 416 if fp is not ui:
413 417 fp.close()
414 418 elif opts.get('mbox'):
415 419 ui.status('Writing ', m['Subject'], ' ...\n')
416 420 fp = open(opts.get('mbox'),
417 421 m.has_key('In-Reply-To') and 'ab+' or 'wb+')
418 422 date = util.datestr(date=start_time,
419 423 format='%a %b %d %H:%M:%S %Y', timezone=False)
420 424 fp.write('From %s %s\n' % (sender_addr, date))
421 425 fp.write(m.as_string(0))
422 426 fp.write('\n\n')
423 427 fp.close()
424 428 else:
425 429 ui.status('Sending ', m['Subject'], ' ...\n')
426 430 # Exim does not remove the Bcc field
427 431 del m['Bcc']
428 432 mail.sendmail(ui, sender, to + bcc + cc, m.as_string(0))
429 433
430 434 cmdtable = {
431 435 "email":
432 436 (patchbomb,
433 [('a', 'attach', None, _('send patches as inline attachments')),
437 [('a', 'attach', None, _('send patches as attachments')),
438 ('i', 'inline', None, _('send patches as inline attachments')),
434 439 ('', 'bcc', [], _('email addresses of blind copy recipients')),
435 440 ('c', 'cc', [], _('email addresses of copy recipients')),
436 441 ('d', 'diffstat', None, _('add diffstat output to messages')),
437 442 ('', 'date', '', _('use the given date as the sending date')),
438 443 ('', 'desc', '', _('use the given file as the series description')),
439 444 ('g', 'git', None, _('use git extended diff format')),
440 445 ('f', 'from', '', _('email address of sender')),
441 446 ('', 'plain', None, _('omit hg patch header')),
442 447 ('n', 'test', None, _('print messages that would be sent')),
443 448 ('m', 'mbox', '',
444 449 _('write messages to mbox file instead of sending them')),
445 450 ('o', 'outgoing', None,
446 451 _('send changes not found in the target repository')),
447 452 ('b', 'bundle', None,
448 453 _('send changes not in target as a binary bundle')),
449 454 ('r', 'rev', [], _('a revision to send')),
450 455 ('s', 'subject', '',
451 456 _('subject of first message (intro or single patch)')),
452 457 ('t', 'to', [], _('email addresses of recipients')),
453 458 ('', 'force', None,
454 459 _('run even when remote repository is unrelated (with -b)')),
455 460 ('', 'base', [],
456 461 _('a base changeset to specify instead of a destination (with -b)')),
457 462 ] + commands.remoteopts,
458 463 _('hg email [OPTION]... [DEST]...'))
459 464 }
General Comments 0
You need to be logged in to leave comments. Login now