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