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