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