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