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