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