##// END OF EJS Templates
patchbomb: minor typo and language fixes
Cédric Duval -
r8512:b87e5ad9 default
parent child Browse files
Show More
@@ -1,502 +1,502 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 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 the first in the series 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 With the -d/--diffstat option, you will be prompted for each changeset
28 28 with a diffstat summary and the changeset summary, so you can be sure
29 29 you are sending the right 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 prompted for an email recipient address, a subject an an introductory
50 prompted for an email recipient address, a subject and an introductory
51 51 message describing the patches of your patchbomb. Then when all is
52 done, patchbomb messages are displayed. If PAGER environment variable
53 is set, your pager will be fired up once for each patchbomb message,
54 so you can verify everything is alright.
52 done, patchbomb messages are displayed. If the PAGER environment
53 variable is set, your pager will be fired up once for each patchbomb
54 message, 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 to be a sendmail compatable mailer or fill out the [smtp] section so
73 to be a sendmail compatible 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 190 The message contains two or three parts. First, the changeset
191 191 description. Next, (optionally) if the diffstat program is
192 192 installed and -d/--diffstat is used, the result of running
193 193 diffstat on the patch. Finally, the patch itself, as generated by
194 194 "hg export".
195 195
196 196 By default the patch is included as text in the email body for
197 197 easy reviewing. Using the -a/--attach option will instead create
198 198 an attachment for the patch. With -i/--inline an inline attachment
199 199 will be created.
200 200
201 201 With -o/--outgoing, emails will be generated for patches not found
202 202 in the destination repository (or only those which are ancestors
203 203 of the specified revisions if any are provided)
204 204
205 205 With -b/--bundle, changesets are selected as for --outgoing, but a
206 206 single email containing a binary Mercurial bundle as an attachment
207 207 will be sent.
208 208
209 209 Examples:
210 210
211 211 hg email -r 3000 # send patch 3000 only
212 212 hg email -r 3000 -r 3001 # send patches 3000 and 3001
213 213 hg email -r 3000:3005 # send patches 3000 through 3005
214 214 hg email 3000 # send patch 3000 (deprecated)
215 215
216 216 hg email -o # send all patches not in default
217 217 hg email -o DEST # send all patches not in DEST
218 218 hg email -o -r 3000 # send all ancestors of 3000 not in default
219 219 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
220 220
221 221 hg email -b # send bundle of all patches not in default
222 222 hg email -b DEST # send bundle of all patches not in DEST
223 223 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
224 224 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
225 225
226 226 Before using this command, you will need to enable email in your
227 227 hgrc. See the [email] section in hgrc(5) for details.
228 228 '''
229 229
230 230 _charsets = mail._charsets(ui)
231 231
232 232 def outgoing(dest, revs):
233 233 '''Return the revisions present locally but not in dest'''
234 234 dest = ui.expandpath(dest or 'default-push', dest or 'default')
235 235 revs = [repo.lookup(rev) for rev in revs]
236 236 other = hg.repository(cmdutil.remoteui(repo, opts), dest)
237 237 ui.status(_('comparing with %s\n') % dest)
238 238 o = repo.findoutgoing(other)
239 239 if not o:
240 240 ui.status(_("no changes found\n"))
241 241 return []
242 242 o = repo.changelog.nodesbetween(o, revs or None)[0]
243 243 return [str(repo.changelog.rev(r)) for r in o]
244 244
245 245 def getpatches(revs):
246 246 for r in cmdutil.revrange(repo, revs):
247 247 output = cStringIO.StringIO()
248 248 patch.export(repo, [r], fp=output,
249 249 opts=patch.diffopts(ui, opts))
250 250 yield output.getvalue().split('\n')
251 251
252 252 def getbundle(dest):
253 253 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
254 254 tmpfn = os.path.join(tmpdir, 'bundle')
255 255 try:
256 256 commands.bundle(ui, repo, tmpfn, dest, **opts)
257 257 return open(tmpfn, 'rb').read()
258 258 finally:
259 259 try:
260 260 os.unlink(tmpfn)
261 261 except:
262 262 pass
263 263 os.rmdir(tmpdir)
264 264
265 265 if not (opts.get('test') or opts.get('mbox')):
266 266 # really sending
267 267 mail.validateconfig(ui)
268 268
269 269 if not (revs or opts.get('rev')
270 270 or opts.get('outgoing') or opts.get('bundle')
271 271 or opts.get('patches')):
272 272 raise util.Abort(_('specify at least one changeset with -r or -o'))
273 273
274 274 if opts.get('outgoing') and opts.get('bundle'):
275 275 raise util.Abort(_("--outgoing mode always on with --bundle;"
276 276 " do not re-specify --outgoing"))
277 277
278 278 if opts.get('outgoing') or opts.get('bundle'):
279 279 if len(revs) > 1:
280 280 raise util.Abort(_("too many destinations"))
281 281 dest = revs and revs[0] or None
282 282 revs = []
283 283
284 284 if opts.get('rev'):
285 285 if revs:
286 286 raise util.Abort(_('use only one form to specify the revision'))
287 287 revs = opts.get('rev')
288 288
289 289 if opts.get('outgoing'):
290 290 revs = outgoing(dest, opts.get('rev'))
291 291 if opts.get('bundle'):
292 292 opts['revs'] = revs
293 293
294 294 # start
295 295 if opts.get('date'):
296 296 start_time = util.parsedate(opts.get('date'))
297 297 else:
298 298 start_time = util.makedate()
299 299
300 300 def genmsgid(id):
301 301 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
302 302
303 303 def getdescription(body, sender):
304 304 if opts.get('desc'):
305 305 body = open(opts.get('desc')).read()
306 306 else:
307 307 ui.write(_('\nWrite the introductory message for the '
308 308 'patch series.\n\n'))
309 309 body = ui.edit(body, sender)
310 310 return body
311 311
312 312 def getpatchmsgs(patches, patchnames=None):
313 313 jumbo = []
314 314 msgs = []
315 315
316 316 ui.write(_('This patch series consists of %d patches.\n\n')
317 317 % len(patches))
318 318
319 319 name = None
320 320 for i, p in enumerate(patches):
321 321 jumbo.extend(p)
322 322 if patchnames:
323 323 name = patchnames[i]
324 324 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
325 325 len(patches), name)
326 326 msgs.append(msg)
327 327
328 328 if len(patches) > 1 or opts.get('intro'):
329 329 tlen = len(str(len(patches)))
330 330
331 331 subj = '[PATCH %0*d of %d] %s' % (
332 332 tlen, 0, len(patches),
333 333 opts.get('subject') or
334 334 prompt(ui, 'Subject:',
335 335 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
336 336
337 337 body = ''
338 338 if opts.get('diffstat'):
339 339 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
340 340 if d:
341 341 body = '\n' + d
342 342
343 343 body = getdescription(body, sender)
344 344 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
345 345 msg['Subject'] = mail.headencode(ui, subj, _charsets,
346 346 opts.get('test'))
347 347
348 348 msgs.insert(0, (msg, subj))
349 349 return msgs
350 350
351 351 def getbundlemsgs(bundle):
352 352 subj = (opts.get('subject')
353 353 or prompt(ui, 'Subject:', 'A bundle for your repository'))
354 354
355 355 body = getdescription('', sender)
356 356 msg = email.MIMEMultipart.MIMEMultipart()
357 357 if body:
358 358 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
359 359 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
360 360 datapart.set_payload(bundle)
361 361 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
362 362 datapart.add_header('Content-Disposition', 'attachment',
363 363 filename=bundlename)
364 364 email.Encoders.encode_base64(datapart)
365 365 msg.attach(datapart)
366 366 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
367 367 return [(msg, subj)]
368 368
369 369 sender = (opts.get('from') or ui.config('email', 'from') or
370 370 ui.config('patchbomb', 'from') or
371 371 prompt(ui, 'From', ui.username()))
372 372
373 373 # internal option used by pbranches
374 374 patches = opts.get('patches')
375 375 if patches:
376 376 msgs = getpatchmsgs(patches, opts.get('patchnames'))
377 377 elif opts.get('bundle'):
378 378 msgs = getbundlemsgs(getbundle(dest))
379 379 else:
380 380 msgs = getpatchmsgs(list(getpatches(revs)))
381 381
382 382 def getaddrs(opt, prpt, default = None):
383 383 addrs = opts.get(opt) or (ui.config('email', opt) or
384 384 ui.config('patchbomb', opt) or
385 385 prompt(ui, prpt, default)).split(',')
386 386 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
387 387 for a in addrs if a.strip()]
388 388
389 389 to = getaddrs('to', 'To')
390 390 cc = getaddrs('cc', 'Cc', '')
391 391
392 392 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
393 393 ui.config('patchbomb', 'bcc') or '').split(',')
394 394 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
395 395 for a in bcc if a.strip()]
396 396
397 397 ui.write('\n')
398 398
399 399 parent = opts.get('in_reply_to') or None
400 400
401 401 sender_addr = email.Utils.parseaddr(sender)[1]
402 402 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
403 403 sendmail = None
404 404 for m, subj in msgs:
405 405 try:
406 406 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
407 407 except TypeError:
408 408 m['Message-Id'] = genmsgid('patchbomb')
409 409 if parent:
410 410 m['In-Reply-To'] = parent
411 411 m['References'] = parent
412 412 else:
413 413 parent = m['Message-Id']
414 414 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
415 415 m['Date'] = util.datestr(start_time, "%a, %d %b %Y %H:%M:%S %1%2")
416 416
417 417 start_time = (start_time[0] + 1, start_time[1])
418 418 m['From'] = sender
419 419 m['To'] = ', '.join(to)
420 420 if cc:
421 421 m['Cc'] = ', '.join(cc)
422 422 if bcc:
423 423 m['Bcc'] = ', '.join(bcc)
424 424 if opts.get('test'):
425 425 ui.status(_('Displaying '), subj, ' ...\n')
426 426 ui.flush()
427 427 if 'PAGER' in os.environ:
428 428 fp = util.popen(os.environ['PAGER'], 'w')
429 429 else:
430 430 fp = ui
431 431 generator = email.Generator.Generator(fp, mangle_from_=False)
432 432 try:
433 433 generator.flatten(m, 0)
434 434 fp.write('\n')
435 435 except IOError, inst:
436 436 if inst.errno != errno.EPIPE:
437 437 raise
438 438 if fp is not ui:
439 439 fp.close()
440 440 elif opts.get('mbox'):
441 441 ui.status(_('Writing '), subj, ' ...\n')
442 442 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
443 443 generator = email.Generator.Generator(fp, mangle_from_=True)
444 444 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
445 445 fp.write('From %s %s\n' % (sender_addr, date))
446 446 generator.flatten(m, 0)
447 447 fp.write('\n\n')
448 448 fp.close()
449 449 else:
450 450 if not sendmail:
451 451 sendmail = mail.connect(ui)
452 452 ui.status(_('Sending '), subj, ' ...\n')
453 453 # Exim does not remove the Bcc field
454 454 del m['Bcc']
455 455 fp = cStringIO.StringIO()
456 456 generator = email.Generator.Generator(fp, mangle_from_=False)
457 457 generator.flatten(m, 0)
458 458 sendmail(sender, to + bcc + cc, fp.getvalue())
459 459
460 460 emailopts = [
461 461 ('a', 'attach', None, _('send patches as attachments')),
462 462 ('i', 'inline', None, _('send patches as inline attachments')),
463 463 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
464 464 ('c', 'cc', [], _('email addresses of copy recipients')),
465 465 ('d', 'diffstat', None, _('add diffstat output to messages')),
466 466 ('', 'date', '', _('use the given date as the sending date')),
467 467 ('', 'desc', '', _('use the given file as the series description')),
468 468 ('f', 'from', '', _('email address of sender')),
469 469 ('n', 'test', None, _('print messages that would be sent')),
470 470 ('m', 'mbox', '',
471 471 _('write messages to mbox file instead of sending them')),
472 472 ('s', 'subject', '',
473 473 _('subject of first message (intro or single patch)')),
474 474 ('', 'in-reply-to', '',
475 475 _('message identifier to reply to')),
476 476 ('t', 'to', [], _('email addresses of recipients')),
477 477 ]
478 478
479 479
480 480 cmdtable = {
481 481 "email":
482 482 (patchbomb,
483 483 [('g', 'git', None, _('use git extended diff format')),
484 484 ('', 'plain', None, _('omit hg patch header')),
485 485 ('o', 'outgoing', None,
486 486 _('send changes not found in the target repository')),
487 487 ('b', 'bundle', None,
488 488 _('send changes not in target as a binary bundle')),
489 489 ('', 'bundlename', 'bundle',
490 490 _('file name of the bundle attachment')),
491 491 ('r', 'rev', [], _('a revision to send')),
492 492 ('', 'force', None,
493 493 _('run even when remote repository is unrelated '
494 494 '(with -b/--bundle)')),
495 495 ('', 'base', [],
496 496 _('a base changeset to specify instead of a destination '
497 497 '(with -b/--bundle)')),
498 498 ('', 'intro', None,
499 499 _('send an introduction email for a single patch')),
500 500 ] + emailopts + commands.remoteopts,
501 501 _('hg email [OPTION]... [DEST]...'))
502 502 }
General Comments 0
You need to be logged in to leave comments. Login now