##// END OF EJS Templates
hgext: more patchbomb documentation...
Giorgos Keramidas -
r2926:13cd2cde default
parent child Browse files
Show More
@@ -1,309 +1,332 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 # To enable this extension:
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.
29 #
27 #
30 # The "-m" (mbox) option will create an mbox file instead of sending
28 # [extensions]
31 # the messages directly. This can be reviewed e.g. with "mutt -R -f mbox",
29 # hgext.patchbomb =
32 # and finally sent with "formail -s sendmail -bm -t < mbox".
33 #
30 #
34 # 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
35 # file:
32 # file:
36 #
33 #
37 # [email]
34 # [email]
38 # from = My Name <my@email>
35 # from = My Name <my@email>
39 # to = recipient1, recipient2, ...
36 # to = recipient1, recipient2, ...
40 # cc = cc1, cc2, ...
37 # cc = cc1, cc2, ...
41 # bcc = bcc1, bcc2, ...
38 # bcc = bcc1, bcc2, ...
39 #
40 # Then you can use the "hg email" command to mail a series of changesets
41 # as a patchbomb.
42 #
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
45 # prompted for an email recipient address, a subject an an introductory
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
48 # you can verify everything is alright.
49 #
50 # The "-m" (mbox) option is also very useful. Instead of previewing
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
53 # mailbox file can be previewed with any mail user agent which supports
54 # UNIX mbox files, i.e. with mutt:
55 #
56 # % mutt -R -f mbox
57 #
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),
60 # to send each message out:
61 #
62 # % formail -s sendmail -bm -t < mbox
63 #
64 # That should be all. Now your patchbomb is on its way out.
42
65
43 from mercurial.demandload import *
66 from mercurial.demandload import *
44 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
67 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
45 mercurial:commands,hg,ui
68 mercurial:commands,hg,ui
46 os errno popen2 socket sys tempfile time''')
69 os errno popen2 socket sys tempfile time''')
47 from mercurial.i18n import gettext as _
70 from mercurial.i18n import gettext as _
48 from mercurial.node import *
71 from mercurial.node import *
49
72
50 try:
73 try:
51 # readline gives raw_input editing capabilities, but is not
74 # readline gives raw_input editing capabilities, but is not
52 # present on windows
75 # present on windows
53 import readline
76 import readline
54 except ImportError: pass
77 except ImportError: pass
55
78
56 def diffstat(patch):
79 def diffstat(patch):
57 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
80 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
58 try:
81 try:
59 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
82 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
60 try:
83 try:
61 for line in patch: print >> p.tochild, line
84 for line in patch: print >> p.tochild, line
62 p.tochild.close()
85 p.tochild.close()
63 if p.wait(): return
86 if p.wait(): return
64 fp = os.fdopen(fd, 'r')
87 fp = os.fdopen(fd, 'r')
65 stat = []
88 stat = []
66 for line in fp: stat.append(line.lstrip())
89 for line in fp: stat.append(line.lstrip())
67 last = stat.pop()
90 last = stat.pop()
68 stat.insert(0, last)
91 stat.insert(0, last)
69 stat = ''.join(stat)
92 stat = ''.join(stat)
70 if stat.startswith('0 files'): raise ValueError
93 if stat.startswith('0 files'): raise ValueError
71 return stat
94 return stat
72 except: raise
95 except: raise
73 finally:
96 finally:
74 try: os.unlink(name)
97 try: os.unlink(name)
75 except: pass
98 except: pass
76
99
77 def patchbomb(ui, repo, *revs, **opts):
100 def patchbomb(ui, repo, *revs, **opts):
78 '''send changesets as a series of patch emails
101 '''send changesets as a series of patch emails
79
102
80 The series starts with a "[PATCH 0 of N]" introduction, which
103 The series starts with a "[PATCH 0 of N]" introduction, which
81 describes the series as a whole.
104 describes the series as a whole.
82
105
83 Each patch email has a Subject line of "[PATCH M of N] ...", using
106 Each patch email has a Subject line of "[PATCH M of N] ...", using
84 the first line of the changeset description as the subject text.
107 the first line of the changeset description as the subject text.
85 The message contains two or three body parts. First, the rest of
108 The message contains two or three body parts. First, the rest of
86 the changeset description. Next, (optionally) if the diffstat
109 the changeset description. Next, (optionally) if the diffstat
87 program is installed, the result of running diffstat on the patch.
110 program is installed, the result of running diffstat on the patch.
88 Finally, the patch itself, as generated by "hg export".'''
111 Finally, the patch itself, as generated by "hg export".'''
89 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
112 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
90 if default: prompt += ' [%s]' % default
113 if default: prompt += ' [%s]' % default
91 prompt += rest
114 prompt += rest
92 while True:
115 while True:
93 r = raw_input(prompt)
116 r = raw_input(prompt)
94 if r: return r
117 if r: return r
95 if default is not None: return default
118 if default is not None: return default
96 if empty_ok: return r
119 if empty_ok: return r
97 ui.warn(_('Please enter a valid value.\n'))
120 ui.warn(_('Please enter a valid value.\n'))
98
121
99 def confirm(s):
122 def confirm(s):
100 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
123 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
101 raise ValueError
124 raise ValueError
102
125
103 def cdiffstat(summary, patch):
126 def cdiffstat(summary, patch):
104 s = diffstat(patch)
127 s = diffstat(patch)
105 if s:
128 if s:
106 if summary:
129 if summary:
107 ui.write(summary, '\n')
130 ui.write(summary, '\n')
108 ui.write(s, '\n')
131 ui.write(s, '\n')
109 confirm(_('Does the diffstat above look okay'))
132 confirm(_('Does the diffstat above look okay'))
110 return s
133 return s
111
134
112 def makepatch(patch, idx, total):
135 def makepatch(patch, idx, total):
113 desc = []
136 desc = []
114 node = None
137 node = None
115 body = ''
138 body = ''
116 for line in patch:
139 for line in patch:
117 if line.startswith('#'):
140 if line.startswith('#'):
118 if line.startswith('# Node ID'): node = line.split()[-1]
141 if line.startswith('# Node ID'): node = line.split()[-1]
119 continue
142 continue
120 if line.startswith('diff -r'): break
143 if line.startswith('diff -r'): break
121 desc.append(line)
144 desc.append(line)
122 if not node: raise ValueError
145 if not node: raise ValueError
123
146
124 #body = ('\n'.join(desc[1:]).strip() or
147 #body = ('\n'.join(desc[1:]).strip() or
125 # 'Patch subject is complete summary.')
148 # 'Patch subject is complete summary.')
126 #body += '\n\n\n'
149 #body += '\n\n\n'
127
150
128 if opts['plain']:
151 if opts['plain']:
129 while patch and patch[0].startswith('# '): patch.pop(0)
152 while patch and patch[0].startswith('# '): patch.pop(0)
130 if patch: patch.pop(0)
153 if patch: patch.pop(0)
131 while patch and not patch[0].strip(): patch.pop(0)
154 while patch and not patch[0].strip(): patch.pop(0)
132 if opts['diffstat']:
155 if opts['diffstat']:
133 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
156 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
134 if opts['attach']:
157 if opts['attach']:
135 msg = email.MIMEMultipart.MIMEMultipart()
158 msg = email.MIMEMultipart.MIMEMultipart()
136 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
159 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
137 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
160 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
138 binnode = bin(node)
161 binnode = bin(node)
139 # if node is mq patch, it will have patch file name as tag
162 # if node is mq patch, it will have patch file name as tag
140 patchname = [t for t in repo.nodetags(binnode)
163 patchname = [t for t in repo.nodetags(binnode)
141 if t.endswith('.patch') or t.endswith('.diff')]
164 if t.endswith('.patch') or t.endswith('.diff')]
142 if patchname:
165 if patchname:
143 patchname = patchname[0]
166 patchname = patchname[0]
144 elif total > 1:
167 elif total > 1:
145 patchname = commands.make_filename(repo, '%b-%n.patch',
168 patchname = commands.make_filename(repo, '%b-%n.patch',
146 binnode, idx, total)
169 binnode, idx, total)
147 else:
170 else:
148 patchname = commands.make_filename(repo, '%b.patch', binnode)
171 patchname = commands.make_filename(repo, '%b.patch', binnode)
149 p['Content-Disposition'] = 'inline; filename=' + patchname
172 p['Content-Disposition'] = 'inline; filename=' + patchname
150 msg.attach(p)
173 msg.attach(p)
151 else:
174 else:
152 body += '\n'.join(patch)
175 body += '\n'.join(patch)
153 msg = email.MIMEText.MIMEText(body)
176 msg = email.MIMEText.MIMEText(body)
154 if total == 1:
177 if total == 1:
155 subj = '[PATCH] ' + desc[0].strip()
178 subj = '[PATCH] ' + desc[0].strip()
156 else:
179 else:
157 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
180 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
158 if subj.endswith('.'): subj = subj[:-1]
181 if subj.endswith('.'): subj = subj[:-1]
159 msg['Subject'] = subj
182 msg['Subject'] = subj
160 msg['X-Mercurial-Node'] = node
183 msg['X-Mercurial-Node'] = node
161 return msg
184 return msg
162
185
163 start_time = int(time.time())
186 start_time = int(time.time())
164
187
165 def genmsgid(id):
188 def genmsgid(id):
166 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
189 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
167
190
168 patches = []
191 patches = []
169
192
170 class exportee:
193 class exportee:
171 def __init__(self, container):
194 def __init__(self, container):
172 self.lines = []
195 self.lines = []
173 self.container = container
196 self.container = container
174 self.name = 'email'
197 self.name = 'email'
175
198
176 def write(self, data):
199 def write(self, data):
177 self.lines.append(data)
200 self.lines.append(data)
178
201
179 def close(self):
202 def close(self):
180 self.container.append(''.join(self.lines).split('\n'))
203 self.container.append(''.join(self.lines).split('\n'))
181 self.lines = []
204 self.lines = []
182
205
183 commands.export(ui, repo, *revs, **{'output': exportee(patches),
206 commands.export(ui, repo, *revs, **{'output': exportee(patches),
184 'switch_parent': False,
207 'switch_parent': False,
185 'text': None})
208 'text': None})
186
209
187 jumbo = []
210 jumbo = []
188 msgs = []
211 msgs = []
189
212
190 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
213 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
191
214
192 for p, i in zip(patches, range(len(patches))):
215 for p, i in zip(patches, range(len(patches))):
193 jumbo.extend(p)
216 jumbo.extend(p)
194 msgs.append(makepatch(p, i + 1, len(patches)))
217 msgs.append(makepatch(p, i + 1, len(patches)))
195
218
196 sender = (opts['from'] or ui.config('email', 'from') or
219 sender = (opts['from'] or ui.config('email', 'from') or
197 ui.config('patchbomb', 'from') or
220 ui.config('patchbomb', 'from') or
198 prompt('From', ui.username()))
221 prompt('From', ui.username()))
199
222
200 def getaddrs(opt, prpt, default = None):
223 def getaddrs(opt, prpt, default = None):
201 addrs = opts[opt] or (ui.config('email', opt) or
224 addrs = opts[opt] or (ui.config('email', opt) or
202 ui.config('patchbomb', opt) or
225 ui.config('patchbomb', opt) or
203 prompt(prpt, default = default)).split(',')
226 prompt(prpt, default = default)).split(',')
204 return [a.strip() for a in addrs if a.strip()]
227 return [a.strip() for a in addrs if a.strip()]
205 to = getaddrs('to', 'To')
228 to = getaddrs('to', 'To')
206 cc = getaddrs('cc', 'Cc', '')
229 cc = getaddrs('cc', 'Cc', '')
207
230
208 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
231 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
209 ui.config('patchbomb', 'bcc') or '').split(',')
232 ui.config('patchbomb', 'bcc') or '').split(',')
210 bcc = [a.strip() for a in bcc if a.strip()]
233 bcc = [a.strip() for a in bcc if a.strip()]
211
234
212 if len(patches) > 1:
235 if len(patches) > 1:
213 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
236 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
214
237
215 subj = '[PATCH 0 of %d] %s' % (
238 subj = '[PATCH 0 of %d] %s' % (
216 len(patches),
239 len(patches),
217 opts['subject'] or
240 opts['subject'] or
218 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
241 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
219
242
220 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
243 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
221
244
222 body = []
245 body = []
223
246
224 while True:
247 while True:
225 try: l = raw_input()
248 try: l = raw_input()
226 except EOFError: break
249 except EOFError: break
227 if l == '.': break
250 if l == '.': break
228 body.append(l)
251 body.append(l)
229
252
230 if opts['diffstat']:
253 if opts['diffstat']:
231 d = cdiffstat(_('Final summary:\n'), jumbo)
254 d = cdiffstat(_('Final summary:\n'), jumbo)
232 if d: body.append('\n' + d)
255 if d: body.append('\n' + d)
233
256
234 body = '\n'.join(body) + '\n'
257 body = '\n'.join(body) + '\n'
235
258
236 msg = email.MIMEText.MIMEText(body)
259 msg = email.MIMEText.MIMEText(body)
237 msg['Subject'] = subj
260 msg['Subject'] = subj
238
261
239 msgs.insert(0, msg)
262 msgs.insert(0, msg)
240
263
241 ui.write('\n')
264 ui.write('\n')
242
265
243 if not opts['test'] and not opts['mbox']:
266 if not opts['test'] and not opts['mbox']:
244 mailer = mail.connect(ui)
267 mailer = mail.connect(ui)
245 parent = None
268 parent = None
246
269
247 # Calculate UTC offset
270 # Calculate UTC offset
248 if time.daylight: offset = time.altzone
271 if time.daylight: offset = time.altzone
249 else: offset = time.timezone
272 else: offset = time.timezone
250 if offset <= 0: sign, offset = '+', -offset
273 if offset <= 0: sign, offset = '+', -offset
251 else: sign = '-'
274 else: sign = '-'
252 offset = '%s%02d%02d' % (sign, offset / 3600, (offset % 3600) / 60)
275 offset = '%s%02d%02d' % (sign, offset / 3600, (offset % 3600) / 60)
253
276
254 sender_addr = email.Utils.parseaddr(sender)[1]
277 sender_addr = email.Utils.parseaddr(sender)[1]
255 for m in msgs:
278 for m in msgs:
256 try:
279 try:
257 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
280 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
258 except TypeError:
281 except TypeError:
259 m['Message-Id'] = genmsgid('patchbomb')
282 m['Message-Id'] = genmsgid('patchbomb')
260 if parent:
283 if parent:
261 m['In-Reply-To'] = parent
284 m['In-Reply-To'] = parent
262 else:
285 else:
263 parent = m['Message-Id']
286 parent = m['Message-Id']
264 m['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(start_time)) + ' ' + offset
287 m['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(start_time)) + ' ' + offset
265
288
266 start_time += 1
289 start_time += 1
267 m['From'] = sender
290 m['From'] = sender
268 m['To'] = ', '.join(to)
291 m['To'] = ', '.join(to)
269 if cc: m['Cc'] = ', '.join(cc)
292 if cc: m['Cc'] = ', '.join(cc)
270 if bcc: m['Bcc'] = ', '.join(bcc)
293 if bcc: m['Bcc'] = ', '.join(bcc)
271 if opts['test']:
294 if opts['test']:
272 ui.status('Displaying ', m['Subject'], ' ...\n')
295 ui.status('Displaying ', m['Subject'], ' ...\n')
273 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
296 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
274 try:
297 try:
275 fp.write(m.as_string(0))
298 fp.write(m.as_string(0))
276 fp.write('\n')
299 fp.write('\n')
277 except IOError, inst:
300 except IOError, inst:
278 if inst.errno != errno.EPIPE:
301 if inst.errno != errno.EPIPE:
279 raise
302 raise
280 fp.close()
303 fp.close()
281 elif opts['mbox']:
304 elif opts['mbox']:
282 ui.status('Writing ', m['Subject'], ' ...\n')
305 ui.status('Writing ', m['Subject'], ' ...\n')
283 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
306 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
284 date = time.asctime(time.localtime(start_time))
307 date = time.asctime(time.localtime(start_time))
285 fp.write('From %s %s\n' % (sender_addr, date))
308 fp.write('From %s %s\n' % (sender_addr, date))
286 fp.write(m.as_string(0))
309 fp.write(m.as_string(0))
287 fp.write('\n\n')
310 fp.write('\n\n')
288 fp.close()
311 fp.close()
289 else:
312 else:
290 ui.status('Sending ', m['Subject'], ' ...\n')
313 ui.status('Sending ', m['Subject'], ' ...\n')
291 # Exim does not remove the Bcc field
314 # Exim does not remove the Bcc field
292 del m['Bcc']
315 del m['Bcc']
293 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
316 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
294
317
295 cmdtable = {
318 cmdtable = {
296 'email':
319 'email':
297 (patchbomb,
320 (patchbomb,
298 [('a', 'attach', None, 'send patches as inline attachments'),
321 [('a', 'attach', None, 'send patches as inline attachments'),
299 ('', 'bcc', [], 'email addresses of blind copy recipients'),
322 ('', 'bcc', [], 'email addresses of blind copy recipients'),
300 ('c', 'cc', [], 'email addresses of copy recipients'),
323 ('c', 'cc', [], 'email addresses of copy recipients'),
301 ('d', 'diffstat', None, 'add diffstat output to messages'),
324 ('d', 'diffstat', None, 'add diffstat output to messages'),
302 ('f', 'from', '', 'email address of sender'),
325 ('f', 'from', '', 'email address of sender'),
303 ('', 'plain', None, 'omit hg patch header'),
326 ('', 'plain', None, 'omit hg patch header'),
304 ('n', 'test', None, 'print messages that would be sent'),
327 ('n', 'test', None, 'print messages that would be sent'),
305 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
328 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
306 ('s', 'subject', '', 'subject of introductory message'),
329 ('s', 'subject', '', 'subject of introductory message'),
307 ('t', 'to', [], 'email addresses of recipients')],
330 ('t', 'to', [], 'email addresses of recipients')],
308 "hg email [OPTION]... [REV]...")
331 "hg email [OPTION]... [REV]...")
309 }
332 }
General Comments 0
You need to be logged in to leave comments. Login now