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