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