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