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