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