##// END OF EJS Templates
notify: correct import of email module, sort stdlib modules to top
Augie Fackler -
r19792:efee1f35 default
parent child Browse files
Show More
@@ -1,410 +1,411 b''
1 1 # notify.py - email notifications for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''hooks for sending email push notifications
9 9
10 10 This extension implements hooks to send email notifications when
11 11 changesets are sent from or received by the local repository.
12 12
13 13 First, enable the extension as explained in :hg:`help extensions`, and
14 14 register the hook you want to run. ``incoming`` and ``changegroup`` hooks
15 15 are run when changesets are received, while ``outgoing`` hooks are for
16 16 changesets sent to another repository::
17 17
18 18 [hooks]
19 19 # one email for each incoming changeset
20 20 incoming.notify = python:hgext.notify.hook
21 21 # one email for all incoming changesets
22 22 changegroup.notify = python:hgext.notify.hook
23 23
24 24 # one email for all outgoing changesets
25 25 outgoing.notify = python:hgext.notify.hook
26 26
27 27 This registers the hooks. To enable notification, subscribers must
28 28 be assigned to repositories. The ``[usersubs]`` section maps multiple
29 29 repositories to a given recipient. The ``[reposubs]`` section maps
30 30 multiple recipients to a single repository::
31 31
32 32 [usersubs]
33 33 # key is subscriber email, value is a comma-separated list of repo patterns
34 34 user@host = pattern
35 35
36 36 [reposubs]
37 37 # key is repo pattern, value is a comma-separated list of subscriber emails
38 38 pattern = user@host
39 39
40 40 A ``pattern`` is a ``glob`` matching the absolute path to a repository,
41 41 optionally combined with a revset expression. A revset expression, if
42 42 present, is separated from the glob by a hash. Example::
43 43
44 44 [reposubs]
45 45 */widgets#branch(release) = qa-team@example.com
46 46
47 47 This sends to ``qa-team@example.com`` whenever a changeset on the ``release``
48 48 branch triggers a notification in any repository ending in ``widgets``.
49 49
50 50 In order to place them under direct user management, ``[usersubs]`` and
51 51 ``[reposubs]`` sections may be placed in a separate ``hgrc`` file and
52 52 incorporated by reference::
53 53
54 54 [notify]
55 55 config = /path/to/subscriptionsfile
56 56
57 57 Notifications will not be sent until the ``notify.test`` value is set
58 58 to ``False``; see below.
59 59
60 60 Notifications content can be tweaked with the following configuration entries:
61 61
62 62 notify.test
63 63 If ``True``, print messages to stdout instead of sending them. Default: True.
64 64
65 65 notify.sources
66 66 Space-separated list of change sources. Notifications are activated only
67 67 when a changeset's source is in this list. Sources may be:
68 68
69 69 :``serve``: changesets received via http or ssh
70 70 :``pull``: changesets received via ``hg pull``
71 71 :``unbundle``: changesets received via ``hg unbundle``
72 72 :``push``: changesets sent or received via ``hg push``
73 73 :``bundle``: changesets sent via ``hg unbundle``
74 74
75 75 Default: serve.
76 76
77 77 notify.strip
78 78 Number of leading slashes to strip from url paths. By default, notifications
79 79 reference repositories with their absolute path. ``notify.strip`` lets you
80 80 turn them into relative paths. For example, ``notify.strip=3`` will change
81 81 ``/long/path/repository`` into ``repository``. Default: 0.
82 82
83 83 notify.domain
84 84 Default email domain for sender or recipients with no explicit domain.
85 85
86 86 notify.style
87 87 Style file to use when formatting emails.
88 88
89 89 notify.template
90 90 Template to use when formatting emails.
91 91
92 92 notify.incoming
93 93 Template to use when run as an incoming hook, overriding ``notify.template``.
94 94
95 95 notify.outgoing
96 96 Template to use when run as an outgoing hook, overriding ``notify.template``.
97 97
98 98 notify.changegroup
99 99 Template to use when running as a changegroup hook, overriding
100 100 ``notify.template``.
101 101
102 102 notify.maxdiff
103 103 Maximum number of diff lines to include in notification email. Set to 0
104 104 to disable the diff, or -1 to include all of it. Default: 300.
105 105
106 106 notify.maxsubject
107 107 Maximum number of characters in email's subject line. Default: 67.
108 108
109 109 notify.diffstat
110 110 Set to True to include a diffstat before diff content. Default: True.
111 111
112 112 notify.merge
113 113 If True, send notifications for merge changesets. Default: True.
114 114
115 115 notify.mbox
116 116 If set, append mails to this mbox file instead of sending. Default: None.
117 117
118 118 notify.fromauthor
119 119 If set, use the committer of the first changeset in a changegroup for
120 120 the "From" field of the notification mail. If not set, take the user
121 121 from the pushing repo. Default: False.
122 122
123 123 If set, the following entries will also be used to customize the
124 124 notifications:
125 125
126 126 email.from
127 127 Email ``From`` address to use if none can be found in the generated
128 128 email content.
129 129
130 130 web.baseurl
131 131 Root repository URL to combine with repository paths when making
132 132 references. See also ``notify.strip``.
133 133
134 134 '''
135 135
136 import email, socket, time
136 137 from mercurial.i18n import _
137 138 from mercurial import patch, cmdutil, templater, util, mail
138 import email.Parser, email.Errors, fnmatch, socket, time
139 import fnmatch
139 140
140 141 testedwith = 'internal'
141 142
142 143 # template for single changeset can include email headers.
143 144 single_template = '''
144 145 Subject: changeset in {webroot}: {desc|firstline|strip}
145 146 From: {author}
146 147
147 148 changeset {node|short} in {root}
148 149 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
149 150 description:
150 151 \t{desc|tabindent|strip}
151 152 '''.lstrip()
152 153
153 154 # template for multiple changesets should not contain email headers,
154 155 # because only first set of headers will be used and result will look
155 156 # strange.
156 157 multiple_template = '''
157 158 changeset {node|short} in {root}
158 159 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
159 160 summary: {desc|firstline}
160 161 '''
161 162
162 163 deftemplates = {
163 164 'changegroup': multiple_template,
164 165 }
165 166
166 167 class notifier(object):
167 168 '''email notification class.'''
168 169
169 170 def __init__(self, ui, repo, hooktype):
170 171 self.ui = ui
171 172 cfg = self.ui.config('notify', 'config')
172 173 if cfg:
173 174 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
174 175 self.repo = repo
175 176 self.stripcount = int(self.ui.config('notify', 'strip', 0))
176 177 self.root = self.strip(self.repo.root)
177 178 self.domain = self.ui.config('notify', 'domain')
178 179 self.mbox = self.ui.config('notify', 'mbox')
179 180 self.test = self.ui.configbool('notify', 'test', True)
180 181 self.charsets = mail._charsets(self.ui)
181 182 self.subs = self.subscribers()
182 183 self.merge = self.ui.configbool('notify', 'merge', True)
183 184
184 185 mapfile = self.ui.config('notify', 'style')
185 186 template = (self.ui.config('notify', hooktype) or
186 187 self.ui.config('notify', 'template'))
187 188 self.t = cmdutil.changeset_templater(self.ui, self.repo,
188 189 False, None, mapfile, False)
189 190 if not mapfile and not template:
190 191 template = deftemplates.get(hooktype) or single_template
191 192 if template:
192 193 template = templater.parsestring(template, quoted=False)
193 194 self.t.use_template(template)
194 195
195 196 def strip(self, path):
196 197 '''strip leading slashes from local path, turn into web-safe path.'''
197 198
198 199 path = util.pconvert(path)
199 200 count = self.stripcount
200 201 while count > 0:
201 202 c = path.find('/')
202 203 if c == -1:
203 204 break
204 205 path = path[c + 1:]
205 206 count -= 1
206 207 return path
207 208
208 209 def fixmail(self, addr):
209 210 '''try to clean up email addresses.'''
210 211
211 212 addr = util.email(addr.strip())
212 213 if self.domain:
213 214 a = addr.find('@localhost')
214 215 if a != -1:
215 216 addr = addr[:a]
216 217 if '@' not in addr:
217 218 return addr + '@' + self.domain
218 219 return addr
219 220
220 221 def subscribers(self):
221 222 '''return list of email addresses of subscribers to this repo.'''
222 223 subs = set()
223 224 for user, pats in self.ui.configitems('usersubs'):
224 225 for pat in pats.split(','):
225 226 if '#' in pat:
226 227 pat, revs = pat.split('#', 1)
227 228 else:
228 229 revs = None
229 230 if fnmatch.fnmatch(self.repo.root, pat.strip()):
230 231 subs.add((self.fixmail(user), revs))
231 232 for pat, users in self.ui.configitems('reposubs'):
232 233 if '#' in pat:
233 234 pat, revs = pat.split('#', 1)
234 235 else:
235 236 revs = None
236 237 if fnmatch.fnmatch(self.repo.root, pat):
237 238 for user in users.split(','):
238 239 subs.add((self.fixmail(user), revs))
239 240 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
240 241 for s, r in sorted(subs)]
241 242
242 243 def node(self, ctx, **props):
243 244 '''format one changeset, unless it is a suppressed merge.'''
244 245 if not self.merge and len(ctx.parents()) > 1:
245 246 return False
246 247 self.t.show(ctx, changes=ctx.changeset(),
247 248 baseurl=self.ui.config('web', 'baseurl'),
248 249 root=self.repo.root, webroot=self.root, **props)
249 250 return True
250 251
251 252 def skipsource(self, source):
252 253 '''true if incoming changes from this source should be skipped.'''
253 254 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
254 255 return source not in ok_sources
255 256
256 257 def send(self, ctx, count, data):
257 258 '''send message.'''
258 259
259 260 # Select subscribers by revset
260 261 subs = set()
261 262 for sub, spec in self.subs:
262 263 if spec is None:
263 264 subs.add(sub)
264 265 continue
265 266 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
266 267 if len(revs):
267 268 subs.add(sub)
268 269 continue
269 270 if len(subs) == 0:
270 271 self.ui.debug('notify: no subscribers to selected repo '
271 272 'and revset\n')
272 273 return
273 274
274 275 p = email.Parser.Parser()
275 276 try:
276 277 msg = p.parsestr(data)
277 278 except email.Errors.MessageParseError, inst:
278 279 raise util.Abort(inst)
279 280
280 281 # store sender and subject
281 282 sender, subject = msg['From'], msg['Subject']
282 283 del msg['From'], msg['Subject']
283 284
284 285 if not msg.is_multipart():
285 286 # create fresh mime message from scratch
286 287 # (multipart templates must take care of this themselves)
287 288 headers = msg.items()
288 289 payload = msg.get_payload()
289 290 # for notification prefer readability over data precision
290 291 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
291 292 # reinstate custom headers
292 293 for k, v in headers:
293 294 msg[k] = v
294 295
295 296 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
296 297
297 298 # try to make subject line exist and be useful
298 299 if not subject:
299 300 if count > 1:
300 301 subject = _('%s: %d new changesets') % (self.root, count)
301 302 else:
302 303 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
303 304 subject = '%s: %s' % (self.root, s)
304 305 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
305 306 if maxsubject:
306 307 subject = util.ellipsis(subject, maxsubject)
307 308 msg['Subject'] = mail.headencode(self.ui, subject,
308 309 self.charsets, self.test)
309 310
310 311 # try to make message have proper sender
311 312 if not sender:
312 313 sender = self.ui.config('email', 'from') or self.ui.username()
313 314 if '@' not in sender or '@localhost' in sender:
314 315 sender = self.fixmail(sender)
315 316 msg['From'] = mail.addressencode(self.ui, sender,
316 317 self.charsets, self.test)
317 318
318 319 msg['X-Hg-Notification'] = 'changeset %s' % ctx
319 320 if not msg['Message-Id']:
320 321 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
321 322 (ctx, int(time.time()),
322 323 hash(self.repo.root), socket.getfqdn()))
323 324 msg['To'] = ', '.join(sorted(subs))
324 325
325 326 msgtext = msg.as_string()
326 327 if self.test:
327 328 self.ui.write(msgtext)
328 329 if not msgtext.endswith('\n'):
329 330 self.ui.write('\n')
330 331 else:
331 332 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
332 333 (len(subs), count))
333 334 mail.sendmail(self.ui, util.email(msg['From']),
334 335 subs, msgtext, mbox=self.mbox)
335 336
336 337 def diff(self, ctx, ref=None):
337 338
338 339 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
339 340 prev = ctx.p1().node()
340 341 ref = ref and ref.node() or ctx.node()
341 342 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
342 343 difflines = ''.join(chunks).splitlines()
343 344
344 345 if self.ui.configbool('notify', 'diffstat', True):
345 346 s = patch.diffstat(difflines)
346 347 # s may be nil, don't include the header if it is
347 348 if s:
348 349 self.ui.write('\ndiffstat:\n\n%s' % s)
349 350
350 351 if maxdiff == 0:
351 352 return
352 353 elif maxdiff > 0 and len(difflines) > maxdiff:
353 354 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
354 355 self.ui.write(msg % (len(difflines), maxdiff))
355 356 difflines = difflines[:maxdiff]
356 357 elif difflines:
357 358 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
358 359
359 360 self.ui.write("\n".join(difflines))
360 361
361 362 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
362 363 '''send email notifications to interested subscribers.
363 364
364 365 if used as changegroup hook, send one email for all changesets in
365 366 changegroup. else send one email per changeset.'''
366 367
367 368 n = notifier(ui, repo, hooktype)
368 369 ctx = repo[node]
369 370
370 371 if not n.subs:
371 372 ui.debug('notify: no subscribers to repository %s\n' % n.root)
372 373 return
373 374 if n.skipsource(source):
374 375 ui.debug('notify: changes have source "%s" - skipping\n' % source)
375 376 return
376 377
377 378 ui.pushbuffer()
378 379 data = ''
379 380 count = 0
380 381 author = ''
381 382 if hooktype == 'changegroup' or hooktype == 'outgoing':
382 383 start, end = ctx.rev(), len(repo)
383 384 for rev in xrange(start, end):
384 385 if n.node(repo[rev]):
385 386 count += 1
386 387 if not author:
387 388 author = repo[rev].user()
388 389 else:
389 390 data += ui.popbuffer()
390 391 ui.note(_('notify: suppressing notification for merge %d:%s\n')
391 392 % (rev, repo[rev].hex()[:12]))
392 393 ui.pushbuffer()
393 394 if count:
394 395 n.diff(ctx, repo['tip'])
395 396 else:
396 397 if not n.node(ctx):
397 398 ui.popbuffer()
398 399 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
399 400 (ctx.rev(), ctx.hex()[:12]))
400 401 return
401 402 count += 1
402 403 n.diff(ctx)
403 404
404 405 data += ui.popbuffer()
405 406 fromauthor = ui.config('notify', 'fromauthor')
406 407 if author and fromauthor:
407 408 data = '\n'.join(['From: %s' % author, data])
408 409
409 410 if count:
410 411 n.send(ctx, count, data)
General Comments 0
You need to be logged in to leave comments. Login now