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