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