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