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