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