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