##// END OF EJS Templates
Add a filename for the bundle
John Goerzen -
r4284:a04141f5 default
parent child Browse files
Show More
@@ -1,416 +1,418 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 try:
74 74 # readline gives raw_input editing capabilities, but is not
75 75 # present on windows
76 76 import readline
77 77 except ImportError: pass
78 78
79 79 def patchbomb(ui, repo, *revs, **opts):
80 80 '''send changesets by email
81 81
82 82 By default, diffs are sent in the format generated by hg export,
83 83 one per message. The series starts with a "[PATCH 0 of N]"
84 84 introduction, which describes the series as a whole.
85 85
86 86 Each patch email has a Subject line of "[PATCH M of N] ...", using
87 87 the first line of the changeset description as the subject text.
88 88 The message contains two or three body parts. First, the rest of
89 89 the changeset description. Next, (optionally) if the diffstat
90 90 program is installed, the result of running diffstat on the patch.
91 91 Finally, the patch itself, as generated by "hg export".
92 92
93 93 With --outgoing, emails will be generated for patches not
94 94 found in the destination repository (or only those which are
95 95 ancestors of the specified revisions if any are provided)
96 96
97 97 With --bundle, changesets are selected as for --outgoing,
98 98 but a single email containing a binary Mercurial bundle as an
99 99 attachment will be sent.
100 100
101 101 Examples:
102 102
103 103 hg email -r 3000 # send patch 3000 only
104 104 hg email -r 3000 -r 3001 # send patches 3000 and 3001
105 105 hg email -r 3000:3005 # send patches 3000 through 3005
106 106 hg email 3000 # send patch 3000 (deprecated)
107 107
108 108 hg email -o # send all patches not in default
109 109 hg email -o DEST # send all patches not in DEST
110 110 hg email -o -r 3000 # send all ancestors of 3000 not in default
111 111 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
112 112
113 113 hg email -b # send bundle of all patches not in default
114 114 hg email -b DEST # send bundle of all patches not in DEST
115 115 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
116 116 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
117 117
118 118 Before using this command, you will need to enable email in your hgrc.
119 119 See the [email] section in hgrc(5) for details.
120 120 '''
121 121
122 122 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
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 # option handling
227 227 commands.setremoteconfig(ui, opts)
228 228 if opts.get('outgoint') and opts.get('bundle'):
229 229 raise util.Abort(_("--outgoing mode always on with --bundle; do not re-specify --outgoing"))
230 230
231 231 if opts.get('outgoing') or opts.get('bundle'):
232 232 if len(revs) > 1:
233 233 raise util.Abort(_("too many destinations"))
234 234 dest = revs and revs[0] or None
235 235 revs = []
236 236
237 237 if opts.get('rev'):
238 238 if revs:
239 239 raise util.Abort(_('use only one form to specify the revision'))
240 240 revs = opts.get('rev')
241 241
242 242 if opts.get('outgoing'):
243 243 revs = outgoing(dest, opts.get('rev'))
244 244 if opts.get('bundle'):
245 245 opts['revs'] = revs
246 246
247 247 # start
248 248 start_time = util.makedate()
249 249
250 250 def genmsgid(id):
251 251 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
252 252
253 253 sender = (opts['from'] or ui.config('email', 'from') or
254 254 ui.config('patchbomb', 'from') or
255 255 prompt('From', ui.username()))
256 256
257 257 def getaddrs(opt, prpt, default = None):
258 258 addrs = opts[opt] or (ui.config('email', opt) or
259 259 ui.config('patchbomb', opt) or
260 260 prompt(prpt, default = default)).split(',')
261 261 return [a.strip() for a in addrs if a.strip()]
262 262
263 263 to = getaddrs('to', 'To')
264 264 cc = getaddrs('cc', 'Cc', '')
265 265
266 266 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
267 267 ui.config('patchbomb', 'bcc') or '').split(',')
268 268 bcc = [a.strip() for a in bcc if a.strip()]
269 269
270 270 def getexportmsgs():
271 271 patches = []
272 272
273 273 class exportee:
274 274 def __init__(self, container):
275 275 self.lines = []
276 276 self.container = container
277 277 self.name = 'email'
278 278
279 279 def write(self, data):
280 280 self.lines.append(data)
281 281
282 282 def close(self):
283 283 self.container.append(''.join(self.lines).split('\n'))
284 284 self.lines = []
285 285
286 286 commands.export(ui, repo, *revs, **{'output': exportee(patches),
287 287 'switch_parent': False,
288 288 'text': None,
289 289 'git': opts.get('git')})
290 290
291 291 jumbo = []
292 292 msgs = []
293 293
294 294 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
295 295
296 296 for p, i in zip(patches, xrange(len(patches))):
297 297 jumbo.extend(p)
298 298 msgs.append(makepatch(p, i + 1, len(patches)))
299 299
300 300 if len(patches) > 1:
301 301 tlen = len(str(len(patches)))
302 302
303 303 subj = '[PATCH %0*d of %d] %s' % (
304 304 tlen, 0,
305 305 len(patches),
306 306 opts['subject'] or
307 307 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
308 308 len(patches))))
309 309
310 310 body = ''
311 311 if opts['diffstat']:
312 312 d = cdiffstat(_('Final summary:\n'), jumbo)
313 313 if d: body = '\n' + d
314 314
315 315 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
316 316 body = ui.edit(body, sender)
317 317
318 318 msg = email.MIMEText.MIMEText(body)
319 319 msg['Subject'] = subj
320 320
321 321 msgs.insert(0, msg)
322 322 return msgs
323 323
324 324 def getbundlemsgs(bundle):
325 325 subj = opts['subject'] or \
326 326 prompt('Subject:', default='A bundle for your repository')
327 327 ui.write(_('\nWrite the introductory message for the bundle.\n\n'))
328 328 body = ui.edit('', sender)
329 329
330 330 msg = email.MIMEMultipart.MIMEMultipart()
331 331 if body:
332 332 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
333 333 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
334 334 datapart.set_payload(bundle)
335 datapart.add_header('Content-Disposition', 'attachment',
336 filename='bundle.hg')
335 337 email.Encoders.encode_base64(datapart)
336 338 msg.attach(datapart)
337 339 msg['Subject'] = subj
338 340 return [msg]
339 341
340 342 if opts.get('bundle'):
341 343 msgs = getbundlemsgs(getbundle(dest))
342 344 else:
343 345 msgs = getexportmsgs()
344 346
345 347 ui.write('\n')
346 348
347 349 if not opts['test'] and not opts['mbox']:
348 350 mailer = mail.connect(ui)
349 351 parent = None
350 352
351 353 sender_addr = email.Utils.parseaddr(sender)[1]
352 354 for m in msgs:
353 355 try:
354 356 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
355 357 except TypeError:
356 358 m['Message-Id'] = genmsgid('patchbomb')
357 359 if parent:
358 360 m['In-Reply-To'] = parent
359 361 else:
360 362 parent = m['Message-Id']
361 363 m['Date'] = util.datestr(date=start_time,
362 364 format="%a, %d %b %Y %H:%M:%S", timezone=True)
363 365
364 366 start_time = (start_time[0] + 1, start_time[1])
365 367 m['From'] = sender
366 368 m['To'] = ', '.join(to)
367 369 if cc: m['Cc'] = ', '.join(cc)
368 370 if bcc: m['Bcc'] = ', '.join(bcc)
369 371 if opts['test']:
370 372 ui.status('Displaying ', m['Subject'], ' ...\n')
371 373 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
372 374 try:
373 375 fp.write(m.as_string(0))
374 376 fp.write('\n')
375 377 except IOError, inst:
376 378 if inst.errno != errno.EPIPE:
377 379 raise
378 380 fp.close()
379 381 elif opts['mbox']:
380 382 ui.status('Writing ', m['Subject'], ' ...\n')
381 383 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
382 384 date = util.datestr(date=start_time,
383 385 format='%a %b %d %H:%M:%S %Y', timezone=False)
384 386 fp.write('From %s %s\n' % (sender_addr, date))
385 387 fp.write(m.as_string(0))
386 388 fp.write('\n\n')
387 389 fp.close()
388 390 else:
389 391 ui.status('Sending ', m['Subject'], ' ...\n')
390 392 # Exim does not remove the Bcc field
391 393 del m['Bcc']
392 394 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
393 395
394 396 cmdtable = {
395 397 'email':
396 398 (patchbomb,
397 399 [('a', 'attach', None, 'send patches as inline attachments'),
398 400 ('', 'bcc', [], 'email addresses of blind copy recipients'),
399 401 ('c', 'cc', [], 'email addresses of copy recipients'),
400 402 ('d', 'diffstat', None, 'add diffstat output to messages'),
401 403 ('g', 'git', None, _('use git extended diff format')),
402 404 ('f', 'from', '', 'email address of sender'),
403 405 ('', 'plain', None, 'omit hg patch header'),
404 406 ('n', 'test', None, 'print messages that would be sent'),
405 407 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
406 408 ('o', 'outgoing', None, _('send changes not found in the target repository')),
407 409 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
408 410 ('r', 'rev', [], _('a revision to send')),
409 411 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
410 412 ('t', 'to', [], 'email addresses of recipients'),
411 413 ('', 'force', None, _('run even when remote repository is unrelated (with -b)')),
412 414 ('', 'base', [],
413 415 _('a base changeset to specify instead of a destination (with -b)'))]
414 416 + commands.remoteopts,
415 417 "hg email [OPTION]... [DEST]...")
416 418 }
General Comments 0
You need to be logged in to leave comments. Login now