##// END OF EJS Templates
Add --git support to hg email
Brendan Cully -
r3030:5a17423f default
parent child Browse files
Show More
@@ -1,332 +1,334 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 from mercurial.demandload import *
67 67 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
68 68 mercurial:commands,hg,mail,ui
69 69 os errno popen2 socket sys tempfile time''')
70 70 from mercurial.i18n import gettext as _
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 diffstat(patch):
80 80 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
81 81 try:
82 82 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
83 83 try:
84 84 for line in patch: print >> p.tochild, line
85 85 p.tochild.close()
86 86 if p.wait(): return
87 87 fp = os.fdopen(fd, 'r')
88 88 stat = []
89 89 for line in fp: stat.append(line.lstrip())
90 90 last = stat.pop()
91 91 stat.insert(0, last)
92 92 stat = ''.join(stat)
93 93 if stat.startswith('0 files'): raise ValueError
94 94 return stat
95 95 except: raise
96 96 finally:
97 97 try: os.unlink(name)
98 98 except: pass
99 99
100 100 def patchbomb(ui, repo, *revs, **opts):
101 101 '''send changesets as a series of patch emails
102 102
103 103 The series starts with a "[PATCH 0 of N]" introduction, which
104 104 describes the series as a whole.
105 105
106 106 Each patch email has a Subject line of "[PATCH M of N] ...", using
107 107 the first line of the changeset description as the subject text.
108 108 The message contains two or three body parts. First, the rest of
109 109 the changeset description. Next, (optionally) if the diffstat
110 110 program is installed, the result of running diffstat on the patch.
111 111 Finally, the patch itself, as generated by "hg export".'''
112 112 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
113 113 if default: prompt += ' [%s]' % default
114 114 prompt += rest
115 115 while True:
116 116 r = raw_input(prompt)
117 117 if r: return r
118 118 if default is not None: return default
119 119 if empty_ok: return r
120 120 ui.warn(_('Please enter a valid value.\n'))
121 121
122 122 def confirm(s):
123 123 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
124 124 raise ValueError
125 125
126 126 def cdiffstat(summary, patch):
127 127 s = diffstat(patch)
128 128 if s:
129 129 if summary:
130 130 ui.write(summary, '\n')
131 131 ui.write(s, '\n')
132 132 confirm(_('Does the diffstat above look okay'))
133 133 return s
134 134
135 135 def makepatch(patch, idx, total):
136 136 desc = []
137 137 node = None
138 138 body = ''
139 139 for line in patch:
140 140 if line.startswith('#'):
141 141 if line.startswith('# Node ID'): node = line.split()[-1]
142 142 continue
143 143 if line.startswith('diff -r'): break
144 144 desc.append(line)
145 145 if not node: raise ValueError
146 146
147 147 #body = ('\n'.join(desc[1:]).strip() or
148 148 # 'Patch subject is complete summary.')
149 149 #body += '\n\n\n'
150 150
151 151 if opts['plain']:
152 152 while patch and patch[0].startswith('# '): patch.pop(0)
153 153 if patch: patch.pop(0)
154 154 while patch and not patch[0].strip(): patch.pop(0)
155 155 if opts['diffstat']:
156 156 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
157 157 if opts['attach']:
158 158 msg = email.MIMEMultipart.MIMEMultipart()
159 159 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
160 160 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
161 161 binnode = bin(node)
162 162 # if node is mq patch, it will have patch file name as tag
163 163 patchname = [t for t in repo.nodetags(binnode)
164 164 if t.endswith('.patch') or t.endswith('.diff')]
165 165 if patchname:
166 166 patchname = patchname[0]
167 167 elif total > 1:
168 168 patchname = commands.make_filename(repo, '%b-%n.patch',
169 169 binnode, idx, total)
170 170 else:
171 171 patchname = commands.make_filename(repo, '%b.patch', binnode)
172 172 p['Content-Disposition'] = 'inline; filename=' + patchname
173 173 msg.attach(p)
174 174 else:
175 175 body += '\n'.join(patch)
176 176 msg = email.MIMEText.MIMEText(body)
177 177 if total == 1:
178 178 subj = '[PATCH] ' + desc[0].strip()
179 179 else:
180 180 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
181 181 if subj.endswith('.'): subj = subj[:-1]
182 182 msg['Subject'] = subj
183 183 msg['X-Mercurial-Node'] = node
184 184 return msg
185 185
186 186 start_time = int(time.time())
187 187
188 188 def genmsgid(id):
189 189 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
190 190
191 191 patches = []
192 192
193 193 class exportee:
194 194 def __init__(self, container):
195 195 self.lines = []
196 196 self.container = container
197 197 self.name = 'email'
198 198
199 199 def write(self, data):
200 200 self.lines.append(data)
201 201
202 202 def close(self):
203 203 self.container.append(''.join(self.lines).split('\n'))
204 204 self.lines = []
205 205
206 206 commands.export(ui, repo, *revs, **{'output': exportee(patches),
207 207 'switch_parent': False,
208 'text': None})
208 'text': None,
209 'git': opts.get('git')})
209 210
210 211 jumbo = []
211 212 msgs = []
212 213
213 214 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
214 215
215 216 for p, i in zip(patches, range(len(patches))):
216 217 jumbo.extend(p)
217 218 msgs.append(makepatch(p, i + 1, len(patches)))
218 219
219 220 sender = (opts['from'] or ui.config('email', 'from') or
220 221 ui.config('patchbomb', 'from') or
221 222 prompt('From', ui.username()))
222 223
223 224 def getaddrs(opt, prpt, default = None):
224 225 addrs = opts[opt] or (ui.config('email', opt) or
225 226 ui.config('patchbomb', opt) or
226 227 prompt(prpt, default = default)).split(',')
227 228 return [a.strip() for a in addrs if a.strip()]
228 229 to = getaddrs('to', 'To')
229 230 cc = getaddrs('cc', 'Cc', '')
230 231
231 232 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
232 233 ui.config('patchbomb', 'bcc') or '').split(',')
233 234 bcc = [a.strip() for a in bcc if a.strip()]
234 235
235 236 if len(patches) > 1:
236 237 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
237 238
238 239 subj = '[PATCH 0 of %d] %s' % (
239 240 len(patches),
240 241 opts['subject'] or
241 242 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
242 243
243 244 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
244 245
245 246 body = []
246 247
247 248 while True:
248 249 try: l = raw_input()
249 250 except EOFError: break
250 251 if l == '.': break
251 252 body.append(l)
252 253
253 254 if opts['diffstat']:
254 255 d = cdiffstat(_('Final summary:\n'), jumbo)
255 256 if d: body.append('\n' + d)
256 257
257 258 body = '\n'.join(body) + '\n'
258 259
259 260 msg = email.MIMEText.MIMEText(body)
260 261 msg['Subject'] = subj
261 262
262 263 msgs.insert(0, msg)
263 264
264 265 ui.write('\n')
265 266
266 267 if not opts['test'] and not opts['mbox']:
267 268 mailer = mail.connect(ui)
268 269 parent = None
269 270
270 271 # Calculate UTC offset
271 272 if time.daylight: offset = time.altzone
272 273 else: offset = time.timezone
273 274 if offset <= 0: sign, offset = '+', -offset
274 275 else: sign = '-'
275 276 offset = '%s%02d%02d' % (sign, offset / 3600, (offset % 3600) / 60)
276 277
277 278 sender_addr = email.Utils.parseaddr(sender)[1]
278 279 for m in msgs:
279 280 try:
280 281 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
281 282 except TypeError:
282 283 m['Message-Id'] = genmsgid('patchbomb')
283 284 if parent:
284 285 m['In-Reply-To'] = parent
285 286 else:
286 287 parent = m['Message-Id']
287 288 m['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(start_time)) + ' ' + offset
288 289
289 290 start_time += 1
290 291 m['From'] = sender
291 292 m['To'] = ', '.join(to)
292 293 if cc: m['Cc'] = ', '.join(cc)
293 294 if bcc: m['Bcc'] = ', '.join(bcc)
294 295 if opts['test']:
295 296 ui.status('Displaying ', m['Subject'], ' ...\n')
296 297 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
297 298 try:
298 299 fp.write(m.as_string(0))
299 300 fp.write('\n')
300 301 except IOError, inst:
301 302 if inst.errno != errno.EPIPE:
302 303 raise
303 304 fp.close()
304 305 elif opts['mbox']:
305 306 ui.status('Writing ', m['Subject'], ' ...\n')
306 307 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
307 308 date = time.asctime(time.localtime(start_time))
308 309 fp.write('From %s %s\n' % (sender_addr, date))
309 310 fp.write(m.as_string(0))
310 311 fp.write('\n\n')
311 312 fp.close()
312 313 else:
313 314 ui.status('Sending ', m['Subject'], ' ...\n')
314 315 # Exim does not remove the Bcc field
315 316 del m['Bcc']
316 317 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
317 318
318 319 cmdtable = {
319 320 'email':
320 321 (patchbomb,
321 322 [('a', 'attach', None, 'send patches as inline attachments'),
322 323 ('', 'bcc', [], 'email addresses of blind copy recipients'),
323 324 ('c', 'cc', [], 'email addresses of copy recipients'),
324 325 ('d', 'diffstat', None, 'add diffstat output to messages'),
326 ('g', 'git', None, _('use git extended diff format')),
325 327 ('f', 'from', '', 'email address of sender'),
326 328 ('', 'plain', None, 'omit hg patch header'),
327 329 ('n', 'test', None, 'print messages that would be sent'),
328 330 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
329 331 ('s', 'subject', '', 'subject of introductory message'),
330 332 ('t', 'to', [], 'email addresses of recipients')],
331 333 "hg email [OPTION]... [REV]...")
332 334 }
General Comments 0
You need to be logged in to leave comments. Login now