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