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