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