##// END OF EJS Templates
patchbomb: add content-disposition to make display inline and add filename...
Vadim Gelfer -
r2708:084f07ca default
parent child Browse files
Show More
@@ -1,293 +1,307 b''
1 # Command for sending a collection of Mercurial changesets as a series
1 # Command for sending a collection of Mercurial changesets as a series
2 # of patch emails.
2 # of patch emails.
3 #
3 #
4 # The series is started off with a "[PATCH 0 of N]" introduction,
4 # The series is started off with a "[PATCH 0 of N]" introduction,
5 # which describes the series as a whole.
5 # which describes the series as a whole.
6 #
6 #
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
8 # the first line of the changeset description as the subject text.
8 # the first line of the changeset description as the subject text.
9 # The message contains two or three body parts:
9 # The message contains two or three body parts:
10 #
10 #
11 # The remainder of the changeset description.
11 # The remainder of the changeset description.
12 #
12 #
13 # [Optional] If the diffstat program is installed, the result of
13 # [Optional] If the diffstat program is installed, the result of
14 # running diffstat on the patch.
14 # running diffstat on the patch.
15 #
15 #
16 # The patch itself, as generated by "hg export".
16 # The patch itself, as generated by "hg export".
17 #
17 #
18 # Each message refers to all of its predecessors using the In-Reply-To
18 # Each message refers to all of its predecessors using the In-Reply-To
19 # and References headers, so they will show up as a sequence in
19 # and References headers, so they will show up as a sequence in
20 # threaded mail and news readers, and in mail archives.
20 # threaded mail and news readers, and in mail archives.
21 #
21 #
22 # For each changeset, you will be prompted with a diffstat summary and
22 # For each changeset, you will be prompted with a diffstat summary and
23 # the changeset summary, so you can be sure you are sending the right
23 # the changeset summary, so you can be sure you are sending the right
24 # changes.
24 # changes.
25 #
25 #
26 # It is best to run this script with the "-n" (test only) flag before
26 # It is best to run this script with the "-n" (test only) flag before
27 # firing it up "for real", in which case it will use your pager to
27 # firing it up "for real", in which case it will use your pager to
28 # display each of the messages that it would send.
28 # display each of the messages that it would send.
29 #
29 #
30 # The "-m" (mbox) option will create an mbox file instead of sending
30 # The "-m" (mbox) option will create an mbox file instead of sending
31 # the messages directly. This can be reviewed e.g. with "mutt -R -f mbox",
31 # the messages directly. This can be reviewed e.g. with "mutt -R -f mbox",
32 # and finally sent with "formail -s sendmail -bm -t < mbox".
32 # and finally sent with "formail -s sendmail -bm -t < mbox".
33 #
33 #
34 # To configure other defaults, add a section like this to your hgrc
34 # To configure other defaults, add a section like this to your hgrc
35 # file:
35 # file:
36 #
36 #
37 # [email]
37 # [email]
38 # from = My Name <my@email>
38 # from = My Name <my@email>
39 # to = recipient1, recipient2, ...
39 # to = recipient1, recipient2, ...
40 # cc = cc1, cc2, ...
40 # cc = cc1, cc2, ...
41 # bcc = bcc1, bcc2, ...
41 # bcc = bcc1, bcc2, ...
42
42
43 from mercurial.demandload import *
43 from mercurial.demandload import *
44 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
44 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
45 mercurial:commands,hg,ui
45 mercurial:commands,hg,ui
46 os errno popen2 socket sys tempfile time''')
46 os errno popen2 socket sys tempfile time''')
47 from mercurial.i18n import gettext as _
47 from mercurial.i18n import gettext as _
48 from mercurial.node import *
48
49
49 try:
50 try:
50 # readline gives raw_input editing capabilities, but is not
51 # readline gives raw_input editing capabilities, but is not
51 # present on windows
52 # present on windows
52 import readline
53 import readline
53 except ImportError: pass
54 except ImportError: pass
54
55
55 def diffstat(patch):
56 def diffstat(patch):
56 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
57 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
57 try:
58 try:
58 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
59 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
59 try:
60 try:
60 for line in patch: print >> p.tochild, line
61 for line in patch: print >> p.tochild, line
61 p.tochild.close()
62 p.tochild.close()
62 if p.wait(): return
63 if p.wait(): return
63 fp = os.fdopen(fd, 'r')
64 fp = os.fdopen(fd, 'r')
64 stat = []
65 stat = []
65 for line in fp: stat.append(line.lstrip())
66 for line in fp: stat.append(line.lstrip())
66 last = stat.pop()
67 last = stat.pop()
67 stat.insert(0, last)
68 stat.insert(0, last)
68 stat = ''.join(stat)
69 stat = ''.join(stat)
69 if stat.startswith('0 files'): raise ValueError
70 if stat.startswith('0 files'): raise ValueError
70 return stat
71 return stat
71 except: raise
72 except: raise
72 finally:
73 finally:
73 try: os.unlink(name)
74 try: os.unlink(name)
74 except: pass
75 except: pass
75
76
76 def patchbomb(ui, repo, *revs, **opts):
77 def patchbomb(ui, repo, *revs, **opts):
77 '''send changesets as a series of patch emails
78 '''send changesets as a series of patch emails
78
79
79 The series starts with a "[PATCH 0 of N]" introduction, which
80 The series starts with a "[PATCH 0 of N]" introduction, which
80 describes the series as a whole.
81 describes the series as a whole.
81
82
82 Each patch email has a Subject line of "[PATCH M of N] ...", using
83 Each patch email has a Subject line of "[PATCH M of N] ...", using
83 the first line of the changeset description as the subject text.
84 the first line of the changeset description as the subject text.
84 The message contains two or three body parts. First, the rest of
85 The message contains two or three body parts. First, the rest of
85 the changeset description. Next, (optionally) if the diffstat
86 the changeset description. Next, (optionally) if the diffstat
86 program is installed, the result of running diffstat on the patch.
87 program is installed, the result of running diffstat on the patch.
87 Finally, the patch itself, as generated by "hg export".'''
88 Finally, the patch itself, as generated by "hg export".'''
88 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
89 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
89 if default: prompt += ' [%s]' % default
90 if default: prompt += ' [%s]' % default
90 prompt += rest
91 prompt += rest
91 while True:
92 while True:
92 r = raw_input(prompt)
93 r = raw_input(prompt)
93 if r: return r
94 if r: return r
94 if default is not None: return default
95 if default is not None: return default
95 if empty_ok: return r
96 if empty_ok: return r
96 ui.warn(_('Please enter a valid value.\n'))
97 ui.warn(_('Please enter a valid value.\n'))
97
98
98 def confirm(s):
99 def confirm(s):
99 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
100 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
100 raise ValueError
101 raise ValueError
101
102
102 def cdiffstat(summary, patch):
103 def cdiffstat(summary, patch):
103 s = diffstat(patch)
104 s = diffstat(patch)
104 if s:
105 if s:
105 if summary:
106 if summary:
106 ui.write(summary, '\n')
107 ui.write(summary, '\n')
107 ui.write(s, '\n')
108 ui.write(s, '\n')
108 confirm(_('Does the diffstat above look okay'))
109 confirm(_('Does the diffstat above look okay'))
109 return s
110 return s
110
111
111 def makepatch(patch, idx, total):
112 def makepatch(patch, idx, total):
112 desc = []
113 desc = []
113 node = None
114 node = None
114 body = ''
115 body = ''
115 for line in patch:
116 for line in patch:
116 if line.startswith('#'):
117 if line.startswith('#'):
117 if line.startswith('# Node ID'): node = line.split()[-1]
118 if line.startswith('# Node ID'): node = line.split()[-1]
118 continue
119 continue
119 if line.startswith('diff -r'): break
120 if line.startswith('diff -r'): break
120 desc.append(line)
121 desc.append(line)
121 if not node: raise ValueError
122 if not node: raise ValueError
122
123
123 #body = ('\n'.join(desc[1:]).strip() or
124 #body = ('\n'.join(desc[1:]).strip() or
124 # 'Patch subject is complete summary.')
125 # 'Patch subject is complete summary.')
125 #body += '\n\n\n'
126 #body += '\n\n\n'
126
127
127 if opts['plain']:
128 if opts['plain']:
128 while patch and patch[0].startswith('# '): patch.pop(0)
129 while patch and patch[0].startswith('# '): patch.pop(0)
129 if patch: patch.pop(0)
130 if patch: patch.pop(0)
130 while patch and not patch[0].strip(): patch.pop(0)
131 while patch and not patch[0].strip(): patch.pop(0)
131 if opts['diffstat']:
132 if opts['diffstat']:
132 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
133 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
133 if opts['attach']:
134 if opts['attach']:
134 msg = email.MIMEMultipart.MIMEMultipart()
135 msg = email.MIMEMultipart.MIMEMultipart()
135 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
136 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
136 msg.attach(email.MIMEText.MIMEText('\n'.join(patch), 'x-patch'))
137 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
138 node = bin(node)
139 # if node is mq patch, it will have patch file name as tag
140 patchname = [t for t in repo.nodetags(node)
141 if t.endswith('.patch') or t.endswith('.diff')]
142 if patchname:
143 patchname = patchname[0]
144 elif total > 1:
145 patchname = commands.make_filename(repo, '%b-%n.patch',
146 node, idx, total)
147 else:
148 patchname = commands.make_filename(repo, '%b.patch', node)
149 p['Content-Disposition'] = 'inline; filename=' + patchname
150 msg.attach(p)
137 else:
151 else:
138 body += '\n'.join(patch)
152 body += '\n'.join(patch)
139 msg = email.MIMEText.MIMEText(body)
153 msg = email.MIMEText.MIMEText(body)
140 if total == 1:
154 if total == 1:
141 subj = '[PATCH] ' + desc[0].strip()
155 subj = '[PATCH] ' + desc[0].strip()
142 else:
156 else:
143 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
157 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
144 if subj.endswith('.'): subj = subj[:-1]
158 if subj.endswith('.'): subj = subj[:-1]
145 msg['Subject'] = subj
159 msg['Subject'] = subj
146 msg['X-Mercurial-Node'] = node
160 msg['X-Mercurial-Node'] = node
147 return msg
161 return msg
148
162
149 start_time = int(time.time())
163 start_time = int(time.time())
150
164
151 def genmsgid(id):
165 def genmsgid(id):
152 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
166 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
153
167
154 patches = []
168 patches = []
155
169
156 class exportee:
170 class exportee:
157 def __init__(self, container):
171 def __init__(self, container):
158 self.lines = []
172 self.lines = []
159 self.container = container
173 self.container = container
160 self.name = 'email'
174 self.name = 'email'
161
175
162 def write(self, data):
176 def write(self, data):
163 self.lines.append(data)
177 self.lines.append(data)
164
178
165 def close(self):
179 def close(self):
166 self.container.append(''.join(self.lines).split('\n'))
180 self.container.append(''.join(self.lines).split('\n'))
167 self.lines = []
181 self.lines = []
168
182
169 commands.export(ui, repo, *revs, **{'output': exportee(patches),
183 commands.export(ui, repo, *revs, **{'output': exportee(patches),
170 'switch_parent': False,
184 'switch_parent': False,
171 'text': None})
185 'text': None})
172
186
173 jumbo = []
187 jumbo = []
174 msgs = []
188 msgs = []
175
189
176 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
190 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
177
191
178 for p, i in zip(patches, range(len(patches))):
192 for p, i in zip(patches, range(len(patches))):
179 jumbo.extend(p)
193 jumbo.extend(p)
180 msgs.append(makepatch(p, i + 1, len(patches)))
194 msgs.append(makepatch(p, i + 1, len(patches)))
181
195
182 sender = (opts['from'] or ui.config('email', 'from') or
196 sender = (opts['from'] or ui.config('email', 'from') or
183 ui.config('patchbomb', 'from') or
197 ui.config('patchbomb', 'from') or
184 prompt('From', ui.username()))
198 prompt('From', ui.username()))
185
199
186 def getaddrs(opt, prpt, default = None):
200 def getaddrs(opt, prpt, default = None):
187 addrs = opts[opt] or (ui.config('email', opt) or
201 addrs = opts[opt] or (ui.config('email', opt) or
188 ui.config('patchbomb', opt) or
202 ui.config('patchbomb', opt) or
189 prompt(prpt, default = default)).split(',')
203 prompt(prpt, default = default)).split(',')
190 return [a.strip() for a in addrs if a.strip()]
204 return [a.strip() for a in addrs if a.strip()]
191 to = getaddrs('to', 'To')
205 to = getaddrs('to', 'To')
192 cc = getaddrs('cc', 'Cc', '')
206 cc = getaddrs('cc', 'Cc', '')
193
207
194 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
208 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
195 ui.config('patchbomb', 'bcc') or '').split(',')
209 ui.config('patchbomb', 'bcc') or '').split(',')
196 bcc = [a.strip() for a in bcc if a.strip()]
210 bcc = [a.strip() for a in bcc if a.strip()]
197
211
198 if len(patches) > 1:
212 if len(patches) > 1:
199 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
213 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
200
214
201 subj = '[PATCH 0 of %d] %s' % (
215 subj = '[PATCH 0 of %d] %s' % (
202 len(patches),
216 len(patches),
203 opts['subject'] or
217 opts['subject'] or
204 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
218 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
205
219
206 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
220 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
207
221
208 body = []
222 body = []
209
223
210 while True:
224 while True:
211 try: l = raw_input()
225 try: l = raw_input()
212 except EOFError: break
226 except EOFError: break
213 if l == '.': break
227 if l == '.': break
214 body.append(l)
228 body.append(l)
215
229
216 if opts['diffstat']:
230 if opts['diffstat']:
217 d = cdiffstat(_('Final summary:\n'), jumbo)
231 d = cdiffstat(_('Final summary:\n'), jumbo)
218 if d: body.append('\n' + d)
232 if d: body.append('\n' + d)
219
233
220 body = '\n'.join(body) + '\n'
234 body = '\n'.join(body) + '\n'
221
235
222 msg = email.MIMEText.MIMEText(body)
236 msg = email.MIMEText.MIMEText(body)
223 msg['Subject'] = subj
237 msg['Subject'] = subj
224
238
225 msgs.insert(0, msg)
239 msgs.insert(0, msg)
226
240
227 ui.write('\n')
241 ui.write('\n')
228
242
229 if not opts['test'] and not opts['mbox']:
243 if not opts['test'] and not opts['mbox']:
230 mail = ui.sendmail()
244 mail = ui.sendmail()
231 parent = None
245 parent = None
232
246
233 # Calculate UTC offset
247 # Calculate UTC offset
234 if time.daylight: offset = time.altzone
248 if time.daylight: offset = time.altzone
235 else: offset = time.timezone
249 else: offset = time.timezone
236 if offset <= 0: sign, offset = '+', -offset
250 if offset <= 0: sign, offset = '+', -offset
237 else: sign = '-'
251 else: sign = '-'
238 offset = '%s%02d%02d' % (sign, offset / 3600, (offset % 3600) / 60)
252 offset = '%s%02d%02d' % (sign, offset / 3600, (offset % 3600) / 60)
239
253
240 sender_addr = email.Utils.parseaddr(sender)[1]
254 sender_addr = email.Utils.parseaddr(sender)[1]
241 for m in msgs:
255 for m in msgs:
242 try:
256 try:
243 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
257 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
244 except TypeError:
258 except TypeError:
245 m['Message-Id'] = genmsgid('patchbomb')
259 m['Message-Id'] = genmsgid('patchbomb')
246 if parent:
260 if parent:
247 m['In-Reply-To'] = parent
261 m['In-Reply-To'] = parent
248 else:
262 else:
249 parent = m['Message-Id']
263 parent = m['Message-Id']
250 m['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(start_time)) + ' ' + offset
264 m['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(start_time)) + ' ' + offset
251
265
252 start_time += 1
266 start_time += 1
253 m['From'] = sender
267 m['From'] = sender
254 m['To'] = ', '.join(to)
268 m['To'] = ', '.join(to)
255 if cc: m['Cc'] = ', '.join(cc)
269 if cc: m['Cc'] = ', '.join(cc)
256 if bcc: m['Bcc'] = ', '.join(bcc)
270 if bcc: m['Bcc'] = ', '.join(bcc)
257 if opts['test']:
271 if opts['test']:
258 ui.status('Displaying ', m['Subject'], ' ...\n')
272 ui.status('Displaying ', m['Subject'], ' ...\n')
259 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
273 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
260 try:
274 try:
261 fp.write(m.as_string(0))
275 fp.write(m.as_string(0))
262 fp.write('\n')
276 fp.write('\n')
263 except IOError, inst:
277 except IOError, inst:
264 if inst.errno != errno.EPIPE:
278 if inst.errno != errno.EPIPE:
265 raise
279 raise
266 fp.close()
280 fp.close()
267 elif opts['mbox']:
281 elif opts['mbox']:
268 ui.status('Writing ', m['Subject'], ' ...\n')
282 ui.status('Writing ', m['Subject'], ' ...\n')
269 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
283 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
270 date = time.asctime(time.localtime(start_time))
284 date = time.asctime(time.localtime(start_time))
271 fp.write('From %s %s\n' % (sender_addr, date))
285 fp.write('From %s %s\n' % (sender_addr, date))
272 fp.write(m.as_string(0))
286 fp.write(m.as_string(0))
273 fp.write('\n\n')
287 fp.write('\n\n')
274 fp.close()
288 fp.close()
275 else:
289 else:
276 ui.status('Sending ', m['Subject'], ' ...\n')
290 ui.status('Sending ', m['Subject'], ' ...\n')
277 mail.sendmail(sender, to + bcc + cc, m.as_string(0))
291 mail.sendmail(sender, to + bcc + cc, m.as_string(0))
278
292
279 cmdtable = {
293 cmdtable = {
280 'email':
294 'email':
281 (patchbomb,
295 (patchbomb,
282 [('a', 'attach', None, 'send patches as inline attachments'),
296 [('a', 'attach', None, 'send patches as inline attachments'),
283 ('', 'bcc', [], 'email addresses of blind copy recipients'),
297 ('', 'bcc', [], 'email addresses of blind copy recipients'),
284 ('c', 'cc', [], 'email addresses of copy recipients'),
298 ('c', 'cc', [], 'email addresses of copy recipients'),
285 ('d', 'diffstat', None, 'add diffstat output to messages'),
299 ('d', 'diffstat', None, 'add diffstat output to messages'),
286 ('f', 'from', '', 'email address of sender'),
300 ('f', 'from', '', 'email address of sender'),
287 ('', 'plain', None, 'omit hg patch header'),
301 ('', 'plain', None, 'omit hg patch header'),
288 ('n', 'test', None, 'print messages that would be sent'),
302 ('n', 'test', None, 'print messages that would be sent'),
289 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
303 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
290 ('s', 'subject', '', 'subject of introductory message'),
304 ('s', 'subject', '', 'subject of introductory message'),
291 ('t', 'to', [], 'email addresses of recipients')],
305 ('t', 'to', [], 'email addresses of recipients')],
292 "hg email [OPTION]... [REV]...")
306 "hg email [OPTION]... [REV]...")
293 }
307 }
General Comments 0
You need to be logged in to leave comments. Login now