##// END OF EJS Templates
patchbomb: update --attach to use cmdutil.make_filename
Brendan Cully -
r3253:1e2941fd default
parent child Browse files
Show More
@@ -1,315 +1,315
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 mercurial:commands,hg,mail,ui,patch
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 patchname = commands.make_filename(repo, '%b-%n.patch',
149 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
150 150 binnode, idx, total)
151 151 else:
152 patchname = commands.make_filename(repo, '%b.patch', binnode)
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 161 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
162 162 if subj.endswith('.'): subj = subj[:-1]
163 163 msg['Subject'] = subj
164 164 msg['X-Mercurial-Node'] = node
165 165 return msg
166 166
167 167 start_time = int(time.time())
168 168
169 169 def genmsgid(id):
170 170 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
171 171
172 172 patches = []
173 173
174 174 class exportee:
175 175 def __init__(self, container):
176 176 self.lines = []
177 177 self.container = container
178 178 self.name = 'email'
179 179
180 180 def write(self, data):
181 181 self.lines.append(data)
182 182
183 183 def close(self):
184 184 self.container.append(''.join(self.lines).split('\n'))
185 185 self.lines = []
186 186
187 187 commands.export(ui, repo, *revs, **{'output': exportee(patches),
188 188 'switch_parent': False,
189 189 'text': None,
190 190 'git': opts.get('git')})
191 191
192 192 jumbo = []
193 193 msgs = []
194 194
195 195 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
196 196
197 197 for p, i in zip(patches, range(len(patches))):
198 198 jumbo.extend(p)
199 199 msgs.append(makepatch(p, i + 1, len(patches)))
200 200
201 201 sender = (opts['from'] or ui.config('email', 'from') or
202 202 ui.config('patchbomb', 'from') or
203 203 prompt('From', ui.username()))
204 204
205 205 def getaddrs(opt, prpt, default = None):
206 206 addrs = opts[opt] or (ui.config('email', opt) or
207 207 ui.config('patchbomb', opt) or
208 208 prompt(prpt, default = default)).split(',')
209 209 return [a.strip() for a in addrs if a.strip()]
210 210 to = getaddrs('to', 'To')
211 211 cc = getaddrs('cc', 'Cc', '')
212 212
213 213 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
214 214 ui.config('patchbomb', 'bcc') or '').split(',')
215 215 bcc = [a.strip() for a in bcc if a.strip()]
216 216
217 217 if len(patches) > 1:
218 218 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
219 219
220 220 subj = '[PATCH 0 of %d] %s' % (
221 221 len(patches),
222 222 opts['subject'] or
223 223 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
224 224
225 225 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
226 226
227 227 body = []
228 228
229 229 while True:
230 230 try: l = raw_input()
231 231 except EOFError: break
232 232 if l == '.': break
233 233 body.append(l)
234 234
235 235 if opts['diffstat']:
236 236 d = cdiffstat(_('Final summary:\n'), jumbo)
237 237 if d: body.append('\n' + d)
238 238
239 239 body = '\n'.join(body) + '\n'
240 240
241 241 msg = email.MIMEText.MIMEText(body)
242 242 msg['Subject'] = subj
243 243
244 244 msgs.insert(0, msg)
245 245
246 246 ui.write('\n')
247 247
248 248 if not opts['test'] and not opts['mbox']:
249 249 mailer = mail.connect(ui)
250 250 parent = None
251 251
252 252 # Calculate UTC offset
253 253 if time.daylight: offset = time.altzone
254 254 else: offset = time.timezone
255 255 if offset <= 0: sign, offset = '+', -offset
256 256 else: sign = '-'
257 257 offset = '%s%02d%02d' % (sign, offset / 3600, (offset % 3600) / 60)
258 258
259 259 sender_addr = email.Utils.parseaddr(sender)[1]
260 260 for m in msgs:
261 261 try:
262 262 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
263 263 except TypeError:
264 264 m['Message-Id'] = genmsgid('patchbomb')
265 265 if parent:
266 266 m['In-Reply-To'] = parent
267 267 else:
268 268 parent = m['Message-Id']
269 269 m['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(start_time)) + ' ' + offset
270 270
271 271 start_time += 1
272 272 m['From'] = sender
273 273 m['To'] = ', '.join(to)
274 274 if cc: m['Cc'] = ', '.join(cc)
275 275 if bcc: m['Bcc'] = ', '.join(bcc)
276 276 if opts['test']:
277 277 ui.status('Displaying ', m['Subject'], ' ...\n')
278 278 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
279 279 try:
280 280 fp.write(m.as_string(0))
281 281 fp.write('\n')
282 282 except IOError, inst:
283 283 if inst.errno != errno.EPIPE:
284 284 raise
285 285 fp.close()
286 286 elif opts['mbox']:
287 287 ui.status('Writing ', m['Subject'], ' ...\n')
288 288 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
289 289 date = time.asctime(time.localtime(start_time))
290 290 fp.write('From %s %s\n' % (sender_addr, date))
291 291 fp.write(m.as_string(0))
292 292 fp.write('\n\n')
293 293 fp.close()
294 294 else:
295 295 ui.status('Sending ', m['Subject'], ' ...\n')
296 296 # Exim does not remove the Bcc field
297 297 del m['Bcc']
298 298 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
299 299
300 300 cmdtable = {
301 301 'email':
302 302 (patchbomb,
303 303 [('a', 'attach', None, 'send patches as inline attachments'),
304 304 ('', 'bcc', [], 'email addresses of blind copy recipients'),
305 305 ('c', 'cc', [], 'email addresses of copy recipients'),
306 306 ('d', 'diffstat', None, 'add diffstat output to messages'),
307 307 ('g', 'git', None, _('use git extended diff format')),
308 308 ('f', 'from', '', 'email address of sender'),
309 309 ('', 'plain', None, 'omit hg patch header'),
310 310 ('n', 'test', None, 'print messages that would be sent'),
311 311 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
312 312 ('s', 'subject', '', 'subject of introductory message'),
313 313 ('t', 'to', [], 'email addresses of recipients')],
314 314 "hg email [OPTION]... [REV]...")
315 315 }
General Comments 0
You need to be logged in to leave comments. Login now