##// END OF EJS Templates
patchbomb: add linebreaks after colons (coding style)
Christian Ebert -
r5785:38cd1ce8 default
parent child Browse files
Show More
@@ -1,447 +1,460 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 if default: prompt += ' [%s]' % default
120 if default:
121 prompt += ' [%s]' % default
121 122 prompt += rest
122 123 while True:
123 124 r = ui.prompt(prompt, default=default)
124 if r: return r
125 if default is not None: return default
126 if empty_ok: return r
125 if r:
126 return r
127 if default is not None:
128 return default
129 if empty_ok:
130 return r
127 131 ui.warn(_('Please enter a valid value.\n'))
128 132
129 133 def confirm(s, denial):
130 134 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
131 135 raise util.Abort(denial)
132 136
133 137 def cdiffstat(summary, patchlines):
134 138 s = patch.diffstat(patchlines)
135 139 if s:
136 140 if summary:
137 141 ui.write(summary, '\n')
138 142 ui.write(s, '\n')
139 143 confirm(_('Does the diffstat above look okay'),
140 144 _('diffstat rejected'))
141 145 elif s is None:
142 146 ui.warn(_('No diffstat information available.\n'))
143 147 s = ''
144 148 return s
145 149
146 150 def makepatch(patch, idx, total):
147 151 desc = []
148 152 node = None
149 153 body = ''
150 154 for line in patch:
151 155 if line.startswith('#'):
152 if line.startswith('# Node ID'): node = line.split()[-1]
156 if line.startswith('# Node ID'):
157 node = line.split()[-1]
153 158 continue
154 159 if (line.startswith('diff -r')
155 160 or line.startswith('diff --git')):
156 161 break
157 162 desc.append(line)
158 if not node: raise ValueError
163 if not node:
164 raise ValueError
159 165
160 166 #body = ('\n'.join(desc[1:]).strip() or
161 167 # 'Patch subject is complete summary.')
162 168 #body += '\n\n\n'
163 169
164 170 if opts['plain']:
165 while patch and patch[0].startswith('# '): patch.pop(0)
166 if patch: patch.pop(0)
167 while patch and not patch[0].strip(): patch.pop(0)
171 while patch and patch[0].startswith('# '):
172 patch.pop(0)
173 if patch:
174 patch.pop(0)
175 while patch and not patch[0].strip():
176 patch.pop(0)
168 177 if opts['diffstat']:
169 178 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
170 179 if opts['attach']:
171 180 msg = email.MIMEMultipart.MIMEMultipart()
172 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
181 if body:
182 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
173 183 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
174 184 binnode = bin(node)
175 185 # if node is mq patch, it will have patch file name as tag
176 186 patchname = [t for t in repo.nodetags(binnode)
177 187 if t.endswith('.patch') or t.endswith('.diff')]
178 188 if patchname:
179 189 patchname = patchname[0]
180 190 elif total > 1:
181 191 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
182 192 binnode, idx, total)
183 193 else:
184 194 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
185 195 p['Content-Disposition'] = 'inline; filename=' + patchname
186 196 msg.attach(p)
187 197 else:
188 198 body += '\n'.join(patch)
189 199 msg = email.MIMEText.MIMEText(body)
190 200
191 201 subj = desc[0].strip().rstrip('. ')
192 202 if total == 1:
193 203 subj = '[PATCH] ' + (opts['subject'] or subj)
194 204 else:
195 205 tlen = len(str(total))
196 206 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
197 207 msg['Subject'] = subj
198 208 msg['X-Mercurial-Node'] = node
199 209 return msg
200 210
201 211 def outgoing(dest, revs):
202 212 '''Return the revisions present locally but not in dest'''
203 213 dest = ui.expandpath(dest or 'default-push', dest or 'default')
204 214 revs = [repo.lookup(rev) for rev in revs]
205 215 other = hg.repository(ui, dest)
206 216 ui.status(_('comparing with %s\n') % dest)
207 217 o = repo.findoutgoing(other)
208 218 if not o:
209 219 ui.status(_("no changes found\n"))
210 220 return []
211 221 o = repo.changelog.nodesbetween(o, revs or None)[0]
212 222 return [str(repo.changelog.rev(r)) for r in o]
213 223
214 224 def getbundle(dest):
215 225 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
216 226 tmpfn = os.path.join(tmpdir, 'bundle')
217 227 try:
218 228 commands.bundle(ui, repo, tmpfn, dest, **opts)
219 229 return open(tmpfn, 'rb').read()
220 230 finally:
221 231 try:
222 232 os.unlink(tmpfn)
223 233 except:
224 234 pass
225 235 os.rmdir(tmpdir)
226 236
227 237 if not (opts['test'] or opts['mbox']):
228 238 # really sending
229 239 mail.validateconfig(ui)
230 240
231 241 if not (revs or opts.get('rev')
232 242 or opts.get('outgoing') or opts.get('bundle')):
233 243 raise util.Abort(_('specify at least one changeset with -r or -o'))
234 244
235 245 cmdutil.setremoteconfig(ui, opts)
236 246 if opts.get('outgoing') and opts.get('bundle'):
237 247 raise util.Abort(_("--outgoing mode always on with --bundle;"
238 248 " do not re-specify --outgoing"))
239 249
240 250 if opts.get('outgoing') or opts.get('bundle'):
241 251 if len(revs) > 1:
242 252 raise util.Abort(_("too many destinations"))
243 253 dest = revs and revs[0] or None
244 254 revs = []
245 255
246 256 if opts.get('rev'):
247 257 if revs:
248 258 raise util.Abort(_('use only one form to specify the revision'))
249 259 revs = opts.get('rev')
250 260
251 261 if opts.get('outgoing'):
252 262 revs = outgoing(dest, opts.get('rev'))
253 263 if opts.get('bundle'):
254 264 opts['revs'] = revs
255 265
256 266 # start
257 267 if opts.get('date'):
258 268 start_time = util.parsedate(opts['date'])
259 269 else:
260 270 start_time = util.makedate()
261 271
262 272 def genmsgid(id):
263 273 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
264 274
265 275 def getdescription(body, sender):
266 276 if opts['desc']:
267 277 body = open(opts['desc']).read()
268 278 else:
269 279 ui.write(_('\nWrite the introductory message for the '
270 280 'patch series.\n\n'))
271 281 body = ui.edit(body, sender)
272 282 return body
273 283
274 284 def getexportmsgs():
275 285 patches = []
276 286
277 287 class exportee:
278 288 def __init__(self, container):
279 289 self.lines = []
280 290 self.container = container
281 291 self.name = 'email'
282 292
283 293 def write(self, data):
284 294 self.lines.append(data)
285 295
286 296 def close(self):
287 297 self.container.append(''.join(self.lines).split('\n'))
288 298 self.lines = []
289 299
290 300 commands.export(ui, repo, *revs, **{'output': exportee(patches),
291 301 'switch_parent': False,
292 302 'text': None,
293 303 'git': opts.get('git')})
294 304
295 305 jumbo = []
296 306 msgs = []
297 307
298 308 ui.write(_('This patch series consists of %d patches.\n\n')
299 309 % len(patches))
300 310
301 311 for p, i in zip(patches, xrange(len(patches))):
302 312 jumbo.extend(p)
303 313 msgs.append(makepatch(p, i + 1, len(patches)))
304 314
305 315 if len(patches) > 1:
306 316 tlen = len(str(len(patches)))
307 317
308 318 subj = '[PATCH %0*d of %d] %s' % (
309 319 tlen, 0,
310 320 len(patches),
311 321 opts['subject'] or
312 322 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
313 323 len(patches))))
314 324
315 325 body = ''
316 326 if opts['diffstat']:
317 327 d = cdiffstat(_('Final summary:\n'), jumbo)
318 if d: body = '\n' + d
328 if d:
329 body = '\n' + d
319 330
320 331 body = getdescription(body, sender)
321 332 msg = email.MIMEText.MIMEText(body)
322 333 msg['Subject'] = subj
323 334
324 335 msgs.insert(0, msg)
325 336 return msgs
326 337
327 338 def getbundlemsgs(bundle):
328 339 subj = (opts['subject']
329 340 or prompt('Subject:', default='A bundle for your repository'))
330 341
331 342 body = getdescription('', sender)
332 343 msg = email.MIMEMultipart.MIMEMultipart()
333 344 if body:
334 345 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
335 346 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
336 347 datapart.set_payload(bundle)
337 348 datapart.add_header('Content-Disposition', 'attachment',
338 349 filename='bundle.hg')
339 350 email.Encoders.encode_base64(datapart)
340 351 msg.attach(datapart)
341 352 msg['Subject'] = subj
342 353 return [msg]
343 354
344 355 sender = (opts['from'] or ui.config('email', 'from') or
345 356 ui.config('patchbomb', 'from') or
346 357 prompt('From', ui.username()))
347 358
348 359 if opts.get('bundle'):
349 360 msgs = getbundlemsgs(getbundle(dest))
350 361 else:
351 362 msgs = getexportmsgs()
352 363
353 364 def getaddrs(opt, prpt, default = None):
354 365 addrs = opts[opt] or (ui.config('email', opt) or
355 366 ui.config('patchbomb', opt) or
356 367 prompt(prpt, default = default)).split(',')
357 368 return [a.strip() for a in addrs if a.strip()]
358 369
359 370 to = getaddrs('to', 'To')
360 371 cc = getaddrs('cc', 'Cc', '')
361 372
362 373 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
363 374 ui.config('patchbomb', 'bcc') or '').split(',')
364 375 bcc = [a.strip() for a in bcc if a.strip()]
365 376
366 377 ui.write('\n')
367 378
368 379 parent = None
369 380
370 381 sender_addr = email.Utils.parseaddr(sender)[1]
371 382 for m in msgs:
372 383 try:
373 384 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
374 385 except TypeError:
375 386 m['Message-Id'] = genmsgid('patchbomb')
376 387 if parent:
377 388 m['In-Reply-To'] = parent
378 389 else:
379 390 parent = m['Message-Id']
380 391 m['Date'] = util.datestr(date=start_time,
381 392 format="%a, %d %b %Y %H:%M:%S", timezone=True)
382 393
383 394 start_time = (start_time[0] + 1, start_time[1])
384 395 m['From'] = sender
385 396 m['To'] = ', '.join(to)
386 if cc: m['Cc'] = ', '.join(cc)
387 if bcc: m['Bcc'] = ', '.join(bcc)
397 if cc:
398 m['Cc'] = ', '.join(cc)
399 if bcc:
400 m['Bcc'] = ', '.join(bcc)
388 401 if opts['test']:
389 402 ui.status('Displaying ', m['Subject'], ' ...\n')
390 403 ui.flush()
391 404 if 'PAGER' in os.environ:
392 405 fp = os.popen(os.environ['PAGER'], 'w')
393 406 else:
394 407 fp = ui
395 408 try:
396 409 fp.write(m.as_string(0))
397 410 fp.write('\n')
398 411 except IOError, inst:
399 412 if inst.errno != errno.EPIPE:
400 413 raise
401 414 if fp is not ui:
402 415 fp.close()
403 416 elif opts['mbox']:
404 417 ui.status('Writing ', m['Subject'], ' ...\n')
405 418 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
406 419 date = util.datestr(date=start_time,
407 420 format='%a %b %d %H:%M:%S %Y', timezone=False)
408 421 fp.write('From %s %s\n' % (sender_addr, date))
409 422 fp.write(m.as_string(0))
410 423 fp.write('\n\n')
411 424 fp.close()
412 425 else:
413 426 ui.status('Sending ', m['Subject'], ' ...\n')
414 427 # Exim does not remove the Bcc field
415 428 del m['Bcc']
416 429 mail.sendmail(ui, sender, to + bcc + cc, m.as_string(0))
417 430
418 431 cmdtable = {
419 432 "email":
420 433 (patchbomb,
421 434 [('a', 'attach', None, _('send patches as inline attachments')),
422 435 ('', 'bcc', [], _('email addresses of blind copy recipients')),
423 436 ('c', 'cc', [], _('email addresses of copy recipients')),
424 437 ('d', 'diffstat', None, _('add diffstat output to messages')),
425 438 ('', 'date', '', _('use the given date as the sending date')),
426 439 ('', 'desc', '', _('use the given file as the series description')),
427 440 ('g', 'git', None, _('use git extended diff format')),
428 441 ('f', 'from', '', _('email address of sender')),
429 442 ('', 'plain', None, _('omit hg patch header')),
430 443 ('n', 'test', None, _('print messages that would be sent')),
431 444 ('m', 'mbox', '',
432 445 _('write messages to mbox file instead of sending them')),
433 446 ('o', 'outgoing', None,
434 447 _('send changes not found in the target repository')),
435 448 ('b', 'bundle', None,
436 449 _('send changes not in target as a binary bundle')),
437 450 ('r', 'rev', [], _('a revision to send')),
438 451 ('s', 'subject', '',
439 452 _('subject of first message (intro or single patch)')),
440 453 ('t', 'to', [], _('email addresses of recipients')),
441 454 ('', 'force', None,
442 455 _('run even when remote repository is unrelated (with -b)')),
443 456 ('', 'base', [],
444 457 _('a base changeset to specify instead of a destination (with -b)')),
445 458 ] + commands.remoteopts,
446 459 _('hg email [OPTION]... [DEST]...'))
447 460 }
General Comments 0
You need to be logged in to leave comments. Login now