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