##// END OF EJS Templates
patchbomb: Fix typo.
Bryan O'Sullivan -
r4492:c15955bd default
parent child Browse files
Show More
@@ -1,421 +1,421 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, your pager will be fired up once for each patchbomb message, so
48 48 # you can verify everything is alright.
49 49 #
50 50 # The "-m" (mbox) option is also very useful. Instead of previewing
51 51 # each patchbomb message in a pager or sending the messages directly,
52 52 # it will create a UNIX mailbox file with the patch emails. This
53 53 # mailbox file can be previewed with any mail user agent which supports
54 54 # UNIX mbox files, i.e. with mutt:
55 55 #
56 56 # % mutt -R -f mbox
57 57 #
58 58 # When you are previewing the patchbomb messages, you can use `formail'
59 59 # (a utility that is commonly installed as part of the procmail package),
60 60 # to send each message out:
61 61 #
62 62 # % formail -s sendmail -bm -t < mbox
63 63 #
64 64 # That should be all. Now your patchbomb is on its way out.
65 65
66 66 import os, errno, socket, tempfile
67 67 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
68 68 import email.Utils, email.Encoders
69 69 from mercurial import cmdutil, commands, hg, mail, ui, patch, util
70 70 from mercurial.i18n import _
71 71 from mercurial.node import *
72 72
73 73 def patchbomb(ui, repo, *revs, **opts):
74 74 '''send changesets by email
75 75
76 76 By default, diffs are sent in the format generated by hg export,
77 77 one per message. The series starts with a "[PATCH 0 of N]"
78 78 introduction, which describes the series as a whole.
79 79
80 80 Each patch email has a Subject line of "[PATCH M of N] ...", using
81 81 the first line of the changeset description as the subject text.
82 82 The message contains two or three body parts. First, the rest of
83 83 the changeset description. Next, (optionally) if the diffstat
84 84 program is installed, the result of running diffstat on the patch.
85 85 Finally, the patch itself, as generated by "hg export".
86 86
87 87 With --outgoing, emails will be generated for patches not
88 88 found in the destination repository (or only those which are
89 89 ancestors of the specified revisions if any are provided)
90 90
91 91 With --bundle, changesets are selected as for --outgoing,
92 92 but a single email containing a binary Mercurial bundle as an
93 93 attachment will be sent.
94 94
95 95 Examples:
96 96
97 97 hg email -r 3000 # send patch 3000 only
98 98 hg email -r 3000 -r 3001 # send patches 3000 and 3001
99 99 hg email -r 3000:3005 # send patches 3000 through 3005
100 100 hg email 3000 # send patch 3000 (deprecated)
101 101
102 102 hg email -o # send all patches not in default
103 103 hg email -o DEST # send all patches not in DEST
104 104 hg email -o -r 3000 # send all ancestors of 3000 not in default
105 105 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
106 106
107 107 hg email -b # send bundle of all patches not in default
108 108 hg email -b DEST # send bundle of all patches not in DEST
109 109 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
110 110 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
111 111
112 112 Before using this command, you will need to enable email in your hgrc.
113 113 See the [email] section in hgrc(5) for details.
114 114 '''
115 115
116 116 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
117 117 try:
118 118 # readline gives raw_input editing capabilities, but is not
119 119 # present on windows
120 120 import readline
121 121 except ImportError: pass
122 122
123 123 if default: prompt += ' [%s]' % default
124 124 prompt += rest
125 125 while True:
126 126 r = raw_input(prompt)
127 127 if r: return r
128 128 if default is not None: return default
129 129 if empty_ok: return r
130 130 ui.warn(_('Please enter a valid value.\n'))
131 131
132 132 def confirm(s):
133 133 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
134 134 raise ValueError
135 135
136 136 def cdiffstat(summary, patchlines):
137 137 s = patch.diffstat(patchlines)
138 138 if s:
139 139 if summary:
140 140 ui.write(summary, '\n')
141 141 ui.write(s, '\n')
142 142 confirm(_('Does the diffstat above look okay'))
143 143 return s
144 144
145 145 def makepatch(patch, idx, total):
146 146 desc = []
147 147 node = None
148 148 body = ''
149 149 for line in patch:
150 150 if line.startswith('#'):
151 151 if line.startswith('# Node ID'): node = line.split()[-1]
152 152 continue
153 153 if (line.startswith('diff -r')
154 154 or line.startswith('diff --git')):
155 155 break
156 156 desc.append(line)
157 157 if not node: raise ValueError
158 158
159 159 #body = ('\n'.join(desc[1:]).strip() or
160 160 # 'Patch subject is complete summary.')
161 161 #body += '\n\n\n'
162 162
163 163 if opts['plain']:
164 164 while patch and patch[0].startswith('# '): patch.pop(0)
165 165 if patch: patch.pop(0)
166 166 while patch and not patch[0].strip(): patch.pop(0)
167 167 if opts['diffstat']:
168 168 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
169 169 if opts['attach']:
170 170 msg = email.MIMEMultipart.MIMEMultipart()
171 171 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
172 172 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
173 173 binnode = bin(node)
174 174 # if node is mq patch, it will have patch file name as tag
175 175 patchname = [t for t in repo.nodetags(binnode)
176 176 if t.endswith('.patch') or t.endswith('.diff')]
177 177 if patchname:
178 178 patchname = patchname[0]
179 179 elif total > 1:
180 180 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
181 181 binnode, idx, total)
182 182 else:
183 183 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
184 184 p['Content-Disposition'] = 'inline; filename=' + patchname
185 185 msg.attach(p)
186 186 else:
187 187 body += '\n'.join(patch)
188 188 msg = email.MIMEText.MIMEText(body)
189 189
190 190 subj = desc[0].strip().rstrip('. ')
191 191 if total == 1:
192 192 subj = '[PATCH] ' + (opts['subject'] or subj)
193 193 else:
194 194 tlen = len(str(total))
195 195 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
196 196 msg['Subject'] = subj
197 197 msg['X-Mercurial-Node'] = node
198 198 return msg
199 199
200 200 def outgoing(dest, revs):
201 201 '''Return the revisions present locally but not in dest'''
202 202 dest = ui.expandpath(dest or 'default-push', dest or 'default')
203 203 revs = [repo.lookup(rev) for rev in revs]
204 204 other = hg.repository(ui, dest)
205 205 ui.status(_('comparing with %s\n') % dest)
206 206 o = repo.findoutgoing(other)
207 207 if not o:
208 208 ui.status(_("no changes found\n"))
209 209 return []
210 210 o = repo.changelog.nodesbetween(o, revs or None)[0]
211 211 return [str(repo.changelog.rev(r)) for r in o]
212 212
213 213 def getbundle(dest):
214 214 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
215 215 tmpfn = os.path.join(tmpdir, 'bundle')
216 216 try:
217 217 commands.bundle(ui, repo, tmpfn, dest, **opts)
218 218 return open(tmpfn).read()
219 219 finally:
220 220 try:
221 221 os.unlink(tmpfn)
222 222 except:
223 223 pass
224 224 os.rmdir(tmpdir)
225 225
226 226 if not opts['test']:
227 227 mail.validateconfig(ui)
228 228
229 229 # option handling
230 230 commands.setremoteconfig(ui, opts)
231 if opts.get('outgoint') and opts.get('bundle'):
231 if opts.get('outgoing') and opts.get('bundle'):
232 232 raise util.Abort(_("--outgoing mode always on with --bundle; do not re-specify --outgoing"))
233 233
234 234 if opts.get('outgoing') or opts.get('bundle'):
235 235 if len(revs) > 1:
236 236 raise util.Abort(_("too many destinations"))
237 237 dest = revs and revs[0] or None
238 238 revs = []
239 239
240 240 if opts.get('rev'):
241 241 if revs:
242 242 raise util.Abort(_('use only one form to specify the revision'))
243 243 revs = opts.get('rev')
244 244
245 245 if opts.get('outgoing'):
246 246 revs = outgoing(dest, opts.get('rev'))
247 247 if opts.get('bundle'):
248 248 opts['revs'] = revs
249 249
250 250 # start
251 251 start_time = util.makedate()
252 252
253 253 def genmsgid(id):
254 254 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
255 255
256 256 def getexportmsgs():
257 257 patches = []
258 258
259 259 class exportee:
260 260 def __init__(self, container):
261 261 self.lines = []
262 262 self.container = container
263 263 self.name = 'email'
264 264
265 265 def write(self, data):
266 266 self.lines.append(data)
267 267
268 268 def close(self):
269 269 self.container.append(''.join(self.lines).split('\n'))
270 270 self.lines = []
271 271
272 272 commands.export(ui, repo, *revs, **{'output': exportee(patches),
273 273 'switch_parent': False,
274 274 'text': None,
275 275 'git': opts.get('git')})
276 276
277 277 jumbo = []
278 278 msgs = []
279 279
280 280 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
281 281
282 282 for p, i in zip(patches, xrange(len(patches))):
283 283 jumbo.extend(p)
284 284 msgs.append(makepatch(p, i + 1, len(patches)))
285 285
286 286 if len(patches) > 1:
287 287 tlen = len(str(len(patches)))
288 288
289 289 subj = '[PATCH %0*d of %d] %s' % (
290 290 tlen, 0,
291 291 len(patches),
292 292 opts['subject'] or
293 293 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
294 294 len(patches))))
295 295
296 296 body = ''
297 297 if opts['diffstat']:
298 298 d = cdiffstat(_('Final summary:\n'), jumbo)
299 299 if d: body = '\n' + d
300 300
301 301 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
302 302 body = ui.edit(body, sender)
303 303
304 304 msg = email.MIMEText.MIMEText(body)
305 305 msg['Subject'] = subj
306 306
307 307 msgs.insert(0, msg)
308 308 return msgs
309 309
310 310 def getbundlemsgs(bundle):
311 311 subj = opts['subject'] or \
312 312 prompt('Subject:', default='A bundle for your repository')
313 313 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
314 314 body = ui.edit('', sender)
315 315
316 316 msg = email.MIMEMultipart.MIMEMultipart()
317 317 if body:
318 318 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
319 319 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
320 320 datapart.set_payload(bundle)
321 321 datapart.add_header('Content-Disposition', 'attachment',
322 322 filename='bundle.hg')
323 323 email.Encoders.encode_base64(datapart)
324 324 msg.attach(datapart)
325 325 msg['Subject'] = subj
326 326 return [msg]
327 327
328 328 sender = (opts['from'] or ui.config('email', 'from') or
329 329 ui.config('patchbomb', 'from') or
330 330 prompt('From', ui.username()))
331 331
332 332 if opts.get('bundle'):
333 333 msgs = getbundlemsgs(getbundle(dest))
334 334 else:
335 335 msgs = getexportmsgs()
336 336
337 337 def getaddrs(opt, prpt, default = None):
338 338 addrs = opts[opt] or (ui.config('email', opt) or
339 339 ui.config('patchbomb', opt) or
340 340 prompt(prpt, default = default)).split(',')
341 341 return [a.strip() for a in addrs if a.strip()]
342 342
343 343 to = getaddrs('to', 'To')
344 344 cc = getaddrs('cc', 'Cc', '')
345 345
346 346 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
347 347 ui.config('patchbomb', 'bcc') or '').split(',')
348 348 bcc = [a.strip() for a in bcc if a.strip()]
349 349
350 350 ui.write('\n')
351 351
352 352 if not opts['test'] and not opts['mbox']:
353 353 mailer = mail.connect(ui)
354 354 parent = None
355 355
356 356 sender_addr = email.Utils.parseaddr(sender)[1]
357 357 for m in msgs:
358 358 try:
359 359 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
360 360 except TypeError:
361 361 m['Message-Id'] = genmsgid('patchbomb')
362 362 if parent:
363 363 m['In-Reply-To'] = parent
364 364 else:
365 365 parent = m['Message-Id']
366 366 m['Date'] = util.datestr(date=start_time,
367 367 format="%a, %d %b %Y %H:%M:%S", timezone=True)
368 368
369 369 start_time = (start_time[0] + 1, start_time[1])
370 370 m['From'] = sender
371 371 m['To'] = ', '.join(to)
372 372 if cc: m['Cc'] = ', '.join(cc)
373 373 if bcc: m['Bcc'] = ', '.join(bcc)
374 374 if opts['test']:
375 375 ui.status('Displaying ', m['Subject'], ' ...\n')
376 376 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
377 377 try:
378 378 fp.write(m.as_string(0))
379 379 fp.write('\n')
380 380 except IOError, inst:
381 381 if inst.errno != errno.EPIPE:
382 382 raise
383 383 fp.close()
384 384 elif opts['mbox']:
385 385 ui.status('Writing ', m['Subject'], ' ...\n')
386 386 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
387 387 date = util.datestr(date=start_time,
388 388 format='%a %b %d %H:%M:%S %Y', timezone=False)
389 389 fp.write('From %s %s\n' % (sender_addr, date))
390 390 fp.write(m.as_string(0))
391 391 fp.write('\n\n')
392 392 fp.close()
393 393 else:
394 394 ui.status('Sending ', m['Subject'], ' ...\n')
395 395 # Exim does not remove the Bcc field
396 396 del m['Bcc']
397 397 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
398 398
399 399 cmdtable = {
400 400 'email':
401 401 (patchbomb,
402 402 [('a', 'attach', None, 'send patches as inline attachments'),
403 403 ('', 'bcc', [], 'email addresses of blind copy recipients'),
404 404 ('c', 'cc', [], 'email addresses of copy recipients'),
405 405 ('d', 'diffstat', None, 'add diffstat output to messages'),
406 406 ('g', 'git', None, _('use git extended diff format')),
407 407 ('f', 'from', '', 'email address of sender'),
408 408 ('', 'plain', None, 'omit hg patch header'),
409 409 ('n', 'test', None, 'print messages that would be sent'),
410 410 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
411 411 ('o', 'outgoing', None, _('send changes not found in the target repository')),
412 412 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
413 413 ('r', 'rev', [], _('a revision to send')),
414 414 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
415 415 ('t', 'to', [], 'email addresses of recipients'),
416 416 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
417 417 ('', 'base', [],
418 418 _('a base changeset to specify instead of a destination (with -b)'))]
419 419 + commands.remoteopts,
420 420 "hg email [OPTION]... [DEST]...")
421 421 }
General Comments 0
You need to be logged in to leave comments. Login now