##// END OF EJS Templates
patchbomb: make "hg email -b" w/o destination work as advertised
Christian Ebert -
r5674:659955e2 default
parent child Browse files
Show More
@@ -1,442 +1,443 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: prompt += ' [%s]' % default
121 121 prompt += rest
122 122 while True:
123 123 r = ui.prompt(prompt, default=default)
124 124 if r: return r
125 125 if default is not None: return default
126 126 if empty_ok: return r
127 127 ui.warn(_('Please enter a valid value.\n'))
128 128
129 129 def confirm(s, denial):
130 130 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
131 131 raise util.Abort(denial)
132 132
133 133 def cdiffstat(summary, patchlines):
134 134 s = patch.diffstat(patchlines)
135 135 if s:
136 136 if summary:
137 137 ui.write(summary, '\n')
138 138 ui.write(s, '\n')
139 139 confirm(_('Does the diffstat above look okay'),
140 140 _('diffstat rejected'))
141 141 elif s is None:
142 142 ui.warn(_('No diffstat information available.\n'))
143 143 s = ''
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 if not (opts['test'] or opts['mbox']):
228 228 # really sending
229 229 mail.validateconfig(ui)
230 230
231 if not (revs or opts.get('rev') or opts.get('outgoing')):
231 if not (revs or opts.get('rev')
232 or opts.get('outgoing') or opts.get('bundle')):
232 233 raise util.Abort(_('specify at least one changeset with -r or -o'))
233 234
234 235 cmdutil.setremoteconfig(ui, opts)
235 236 if opts.get('outgoing') and opts.get('bundle'):
236 237 raise util.Abort(_("--outgoing mode always on with --bundle; do not re-specify --outgoing"))
237 238
238 239 if opts.get('outgoing') or opts.get('bundle'):
239 240 if len(revs) > 1:
240 241 raise util.Abort(_("too many destinations"))
241 242 dest = revs and revs[0] or None
242 243 revs = []
243 244
244 245 if opts.get('rev'):
245 246 if revs:
246 247 raise util.Abort(_('use only one form to specify the revision'))
247 248 revs = opts.get('rev')
248 249
249 250 if opts.get('outgoing'):
250 251 revs = outgoing(dest, opts.get('rev'))
251 252 if opts.get('bundle'):
252 253 opts['revs'] = revs
253 254
254 255 # start
255 256 if opts.get('date'):
256 257 start_time = util.parsedate(opts['date'])
257 258 else:
258 259 start_time = util.makedate()
259 260
260 261 def genmsgid(id):
261 262 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
262 263
263 264 def getexportmsgs():
264 265 patches = []
265 266
266 267 class exportee:
267 268 def __init__(self, container):
268 269 self.lines = []
269 270 self.container = container
270 271 self.name = 'email'
271 272
272 273 def write(self, data):
273 274 self.lines.append(data)
274 275
275 276 def close(self):
276 277 self.container.append(''.join(self.lines).split('\n'))
277 278 self.lines = []
278 279
279 280 commands.export(ui, repo, *revs, **{'output': exportee(patches),
280 281 'switch_parent': False,
281 282 'text': None,
282 283 'git': opts.get('git')})
283 284
284 285 jumbo = []
285 286 msgs = []
286 287
287 288 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
288 289
289 290 for p, i in zip(patches, xrange(len(patches))):
290 291 jumbo.extend(p)
291 292 msgs.append(makepatch(p, i + 1, len(patches)))
292 293
293 294 if len(patches) > 1:
294 295 tlen = len(str(len(patches)))
295 296
296 297 subj = '[PATCH %0*d of %d] %s' % (
297 298 tlen, 0,
298 299 len(patches),
299 300 opts['subject'] or
300 301 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
301 302 len(patches))))
302 303
303 304 body = ''
304 305 if opts['diffstat']:
305 306 d = cdiffstat(_('Final summary:\n'), jumbo)
306 307 if d: body = '\n' + d
307 308
308 309 if opts['desc']:
309 310 body = open(opts['desc']).read()
310 311 else:
311 312 ui.write(_('\nWrite the introductory message for the '
312 313 'patch series.\n\n'))
313 314 body = ui.edit(body, sender)
314 315
315 316 msg = email.MIMEText.MIMEText(body)
316 317 msg['Subject'] = subj
317 318
318 319 msgs.insert(0, msg)
319 320 return msgs
320 321
321 322 def getbundlemsgs(bundle):
322 323 subj = (opts['subject']
323 324 or prompt('Subject:', default='A bundle for your repository'))
324 325 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
325 326 body = ui.edit('', sender)
326 327
327 328 msg = email.MIMEMultipart.MIMEMultipart()
328 329 if body:
329 330 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
330 331 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
331 332 datapart.set_payload(bundle)
332 333 datapart.add_header('Content-Disposition', 'attachment',
333 334 filename='bundle.hg')
334 335 email.Encoders.encode_base64(datapart)
335 336 msg.attach(datapart)
336 337 msg['Subject'] = subj
337 338 return [msg]
338 339
339 340 sender = (opts['from'] or ui.config('email', 'from') or
340 341 ui.config('patchbomb', 'from') or
341 342 prompt('From', ui.username()))
342 343
343 344 if opts.get('bundle'):
344 345 msgs = getbundlemsgs(getbundle(dest))
345 346 else:
346 347 msgs = getexportmsgs()
347 348
348 349 def getaddrs(opt, prpt, default = None):
349 350 addrs = opts[opt] or (ui.config('email', opt) or
350 351 ui.config('patchbomb', opt) or
351 352 prompt(prpt, default = default)).split(',')
352 353 return [a.strip() for a in addrs if a.strip()]
353 354
354 355 to = getaddrs('to', 'To')
355 356 cc = getaddrs('cc', 'Cc', '')
356 357
357 358 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
358 359 ui.config('patchbomb', 'bcc') or '').split(',')
359 360 bcc = [a.strip() for a in bcc if a.strip()]
360 361
361 362 ui.write('\n')
362 363
363 364 parent = None
364 365
365 366 sender_addr = email.Utils.parseaddr(sender)[1]
366 367 for m in msgs:
367 368 try:
368 369 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
369 370 except TypeError:
370 371 m['Message-Id'] = genmsgid('patchbomb')
371 372 if parent:
372 373 m['In-Reply-To'] = parent
373 374 else:
374 375 parent = m['Message-Id']
375 376 m['Date'] = util.datestr(date=start_time,
376 377 format="%a, %d %b %Y %H:%M:%S", timezone=True)
377 378
378 379 start_time = (start_time[0] + 1, start_time[1])
379 380 m['From'] = sender
380 381 m['To'] = ', '.join(to)
381 382 if cc: m['Cc'] = ', '.join(cc)
382 383 if bcc: m['Bcc'] = ', '.join(bcc)
383 384 if opts['test']:
384 385 ui.status('Displaying ', m['Subject'], ' ...\n')
385 386 ui.flush()
386 387 if 'PAGER' in os.environ:
387 388 fp = os.popen(os.environ['PAGER'], 'w')
388 389 else:
389 390 fp = ui
390 391 try:
391 392 fp.write(m.as_string(0))
392 393 fp.write('\n')
393 394 except IOError, inst:
394 395 if inst.errno != errno.EPIPE:
395 396 raise
396 397 if fp is not ui:
397 398 fp.close()
398 399 elif opts['mbox']:
399 400 ui.status('Writing ', m['Subject'], ' ...\n')
400 401 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
401 402 date = util.datestr(date=start_time,
402 403 format='%a %b %d %H:%M:%S %Y', timezone=False)
403 404 fp.write('From %s %s\n' % (sender_addr, date))
404 405 fp.write(m.as_string(0))
405 406 fp.write('\n\n')
406 407 fp.close()
407 408 else:
408 409 ui.status('Sending ', m['Subject'], ' ...\n')
409 410 # Exim does not remove the Bcc field
410 411 del m['Bcc']
411 412 mail.sendmail(ui, sender, to + bcc + cc, m.as_string(0))
412 413
413 414 cmdtable = {
414 415 "email":
415 416 (patchbomb,
416 417 [('a', 'attach', None, _('send patches as inline attachments')),
417 418 ('', 'bcc', [], _('email addresses of blind copy recipients')),
418 419 ('c', 'cc', [], _('email addresses of copy recipients')),
419 420 ('d', 'diffstat', None, _('add diffstat output to messages')),
420 421 ('', 'date', '', _('use the given date as the sending date')),
421 422 ('', 'desc', '', _('use the given file as the series description')),
422 423 ('g', 'git', None, _('use git extended diff format')),
423 424 ('f', 'from', '', _('email address of sender')),
424 425 ('', 'plain', None, _('omit hg patch header')),
425 426 ('n', 'test', None, _('print messages that would be sent')),
426 427 ('m', 'mbox', '',
427 428 _('write messages to mbox file instead of sending them')),
428 429 ('o', 'outgoing', None,
429 430 _('send changes not found in the target repository')),
430 431 ('b', 'bundle', None,
431 432 _('send changes not in target as a binary bundle')),
432 433 ('r', 'rev', [], _('a revision to send')),
433 434 ('s', 'subject', '',
434 435 _('subject of first message (intro or single patch)')),
435 436 ('t', 'to', [], _('email addresses of recipients')),
436 437 ('', 'force', None,
437 438 _('run even when remote repository is unrelated (with -b)')),
438 439 ('', 'base', [],
439 440 _('a base changeset to specify instead of a destination (with -b)')),
440 441 ] + commands.remoteopts,
441 442 _('hg email [OPTION]... [DEST]...'))
442 443 }
General Comments 0
You need to be logged in to leave comments. Login now