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