##// END OF EJS Templates
patchbomb: Strip more than one trailing dot (and spaces between them)
Thomas Arendsen Hein -
r4142:ba3e1330 default
parent child Browse files
Show More
@@ -1,317 +1,316 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
158
159 subj = desc[0].strip()
159 subj = desc[0].strip().rstrip('. ')
160 if subj.endswith('.'): subj = subj[:-1]
161 if total == 1:
160 if total == 1:
162 subj = '[PATCH] ' + (opts['subject'] or subj)
161 subj = '[PATCH] ' + (opts['subject'] or subj)
163 else:
162 else:
164 tlen = len(str(total))
163 tlen = len(str(total))
165 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
164 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
166 msg['Subject'] = subj
165 msg['Subject'] = subj
167 msg['X-Mercurial-Node'] = node
166 msg['X-Mercurial-Node'] = node
168 return msg
167 return msg
169
168
170 start_time = util.makedate()
169 start_time = util.makedate()
171
170
172 def genmsgid(id):
171 def genmsgid(id):
173 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
172 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
174
173
175 patches = []
174 patches = []
176
175
177 class exportee:
176 class exportee:
178 def __init__(self, container):
177 def __init__(self, container):
179 self.lines = []
178 self.lines = []
180 self.container = container
179 self.container = container
181 self.name = 'email'
180 self.name = 'email'
182
181
183 def write(self, data):
182 def write(self, data):
184 self.lines.append(data)
183 self.lines.append(data)
185
184
186 def close(self):
185 def close(self):
187 self.container.append(''.join(self.lines).split('\n'))
186 self.container.append(''.join(self.lines).split('\n'))
188 self.lines = []
187 self.lines = []
189
188
190 commands.export(ui, repo, *revs, **{'output': exportee(patches),
189 commands.export(ui, repo, *revs, **{'output': exportee(patches),
191 'switch_parent': False,
190 'switch_parent': False,
192 'text': None,
191 'text': None,
193 'git': opts.get('git')})
192 'git': opts.get('git')})
194
193
195 jumbo = []
194 jumbo = []
196 msgs = []
195 msgs = []
197
196
198 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
197 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
199
198
200 for p, i in zip(patches, xrange(len(patches))):
199 for p, i in zip(patches, xrange(len(patches))):
201 jumbo.extend(p)
200 jumbo.extend(p)
202 msgs.append(makepatch(p, i + 1, len(patches)))
201 msgs.append(makepatch(p, i + 1, len(patches)))
203
202
204 sender = (opts['from'] or ui.config('email', 'from') or
203 sender = (opts['from'] or ui.config('email', 'from') or
205 ui.config('patchbomb', 'from') or
204 ui.config('patchbomb', 'from') or
206 prompt('From', ui.username()))
205 prompt('From', ui.username()))
207
206
208 def getaddrs(opt, prpt, default = None):
207 def getaddrs(opt, prpt, default = None):
209 addrs = opts[opt] or (ui.config('email', opt) or
208 addrs = opts[opt] or (ui.config('email', opt) or
210 ui.config('patchbomb', opt) or
209 ui.config('patchbomb', opt) or
211 prompt(prpt, default = default)).split(',')
210 prompt(prpt, default = default)).split(',')
212 return [a.strip() for a in addrs if a.strip()]
211 return [a.strip() for a in addrs if a.strip()]
213 to = getaddrs('to', 'To')
212 to = getaddrs('to', 'To')
214 cc = getaddrs('cc', 'Cc', '')
213 cc = getaddrs('cc', 'Cc', '')
215
214
216 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
215 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
217 ui.config('patchbomb', 'bcc') or '').split(',')
216 ui.config('patchbomb', 'bcc') or '').split(',')
218 bcc = [a.strip() for a in bcc if a.strip()]
217 bcc = [a.strip() for a in bcc if a.strip()]
219
218
220 if len(patches) > 1:
219 if len(patches) > 1:
221 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
220 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
222
221
223 tlen = len(str(len(patches)))
222 tlen = len(str(len(patches)))
224
223
225 subj = '[PATCH %0*d of %d] %s' % (
224 subj = '[PATCH %0*d of %d] %s' % (
226 tlen, 0,
225 tlen, 0,
227 len(patches),
226 len(patches),
228 opts['subject'] or
227 opts['subject'] or
229 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
228 prompt('Subject:', rest = ' [PATCH %0*d of %d] ' % (tlen, 0,
230 len(patches))))
229 len(patches))))
231
230
232 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
231 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
233
232
234 body = []
233 body = []
235
234
236 while True:
235 while True:
237 try: l = raw_input()
236 try: l = raw_input()
238 except EOFError: break
237 except EOFError: break
239 if l == '.': break
238 if l == '.': break
240 body.append(l)
239 body.append(l)
241
240
242 if opts['diffstat']:
241 if opts['diffstat']:
243 d = cdiffstat(_('Final summary:\n'), jumbo)
242 d = cdiffstat(_('Final summary:\n'), jumbo)
244 if d: body.append('\n' + d)
243 if d: body.append('\n' + d)
245
244
246 body = '\n'.join(body) + '\n'
245 body = '\n'.join(body) + '\n'
247
246
248 msg = email.MIMEText.MIMEText(body)
247 msg = email.MIMEText.MIMEText(body)
249 msg['Subject'] = subj
248 msg['Subject'] = subj
250
249
251 msgs.insert(0, msg)
250 msgs.insert(0, msg)
252
251
253 ui.write('\n')
252 ui.write('\n')
254
253
255 if not opts['test'] and not opts['mbox']:
254 if not opts['test'] and not opts['mbox']:
256 mailer = mail.connect(ui)
255 mailer = mail.connect(ui)
257 parent = None
256 parent = None
258
257
259 sender_addr = email.Utils.parseaddr(sender)[1]
258 sender_addr = email.Utils.parseaddr(sender)[1]
260 for m in msgs:
259 for m in msgs:
261 try:
260 try:
262 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
261 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
263 except TypeError:
262 except TypeError:
264 m['Message-Id'] = genmsgid('patchbomb')
263 m['Message-Id'] = genmsgid('patchbomb')
265 if parent:
264 if parent:
266 m['In-Reply-To'] = parent
265 m['In-Reply-To'] = parent
267 else:
266 else:
268 parent = m['Message-Id']
267 parent = m['Message-Id']
269 m['Date'] = util.datestr(date=start_time,
268 m['Date'] = util.datestr(date=start_time,
270 format="%a, %d %b %Y %H:%M:%S", timezone=True)
269 format="%a, %d %b %Y %H:%M:%S", timezone=True)
271
270
272 start_time = (start_time[0] + 1, start_time[1])
271 start_time = (start_time[0] + 1, start_time[1])
273 m['From'] = sender
272 m['From'] = sender
274 m['To'] = ', '.join(to)
273 m['To'] = ', '.join(to)
275 if cc: m['Cc'] = ', '.join(cc)
274 if cc: m['Cc'] = ', '.join(cc)
276 if bcc: m['Bcc'] = ', '.join(bcc)
275 if bcc: m['Bcc'] = ', '.join(bcc)
277 if opts['test']:
276 if opts['test']:
278 ui.status('Displaying ', m['Subject'], ' ...\n')
277 ui.status('Displaying ', m['Subject'], ' ...\n')
279 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
278 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
280 try:
279 try:
281 fp.write(m.as_string(0))
280 fp.write(m.as_string(0))
282 fp.write('\n')
281 fp.write('\n')
283 except IOError, inst:
282 except IOError, inst:
284 if inst.errno != errno.EPIPE:
283 if inst.errno != errno.EPIPE:
285 raise
284 raise
286 fp.close()
285 fp.close()
287 elif opts['mbox']:
286 elif opts['mbox']:
288 ui.status('Writing ', m['Subject'], ' ...\n')
287 ui.status('Writing ', m['Subject'], ' ...\n')
289 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
288 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
290 date = util.datestr(date=start_time,
289 date = util.datestr(date=start_time,
291 format='%a %b %d %H:%M:%S %Y', timezone=False)
290 format='%a %b %d %H:%M:%S %Y', timezone=False)
292 fp.write('From %s %s\n' % (sender_addr, date))
291 fp.write('From %s %s\n' % (sender_addr, date))
293 fp.write(m.as_string(0))
292 fp.write(m.as_string(0))
294 fp.write('\n\n')
293 fp.write('\n\n')
295 fp.close()
294 fp.close()
296 else:
295 else:
297 ui.status('Sending ', m['Subject'], ' ...\n')
296 ui.status('Sending ', m['Subject'], ' ...\n')
298 # Exim does not remove the Bcc field
297 # Exim does not remove the Bcc field
299 del m['Bcc']
298 del m['Bcc']
300 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
299 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
301
300
302 cmdtable = {
301 cmdtable = {
303 'email':
302 'email':
304 (patchbomb,
303 (patchbomb,
305 [('a', 'attach', None, 'send patches as inline attachments'),
304 [('a', 'attach', None, 'send patches as inline attachments'),
306 ('', 'bcc', [], 'email addresses of blind copy recipients'),
305 ('', 'bcc', [], 'email addresses of blind copy recipients'),
307 ('c', 'cc', [], 'email addresses of copy recipients'),
306 ('c', 'cc', [], 'email addresses of copy recipients'),
308 ('d', 'diffstat', None, 'add diffstat output to messages'),
307 ('d', 'diffstat', None, 'add diffstat output to messages'),
309 ('g', 'git', None, _('use git extended diff format')),
308 ('g', 'git', None, _('use git extended diff format')),
310 ('f', 'from', '', 'email address of sender'),
309 ('f', 'from', '', 'email address of sender'),
311 ('', 'plain', None, 'omit hg patch header'),
310 ('', 'plain', None, 'omit hg patch header'),
312 ('n', 'test', None, 'print messages that would be sent'),
311 ('n', 'test', None, 'print messages that would be sent'),
313 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
312 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
314 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
313 ('s', 'subject', '', 'subject of first message (intro or single patch)'),
315 ('t', 'to', [], 'email addresses of recipients')],
314 ('t', 'to', [], 'email addresses of recipients')],
316 "hg email [OPTION]... [REV]...")
315 "hg email [OPTION]... [REV]...")
317 }
316 }
General Comments 0
You need to be logged in to leave comments. Login now