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