##// END OF EJS Templates
notify: adapt to new location of email module's errors...
Augie Fackler -
r40320:46b55f27 default
parent child Browse files
Show More
@@ -1,511 +1,517
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.maxdiffstat
107 107 Maximum number of diffstat lines to include in notification email. Set to -1
108 108 to include all of it. Default: -1.
109 109
110 110 notify.maxsubject
111 111 Maximum number of characters in email's subject line. Default: 67.
112 112
113 113 notify.diffstat
114 114 Set to True to include a diffstat before diff content. Default: True.
115 115
116 116 notify.showfunc
117 117 If set, override ``diff.showfunc`` for the diff content. Default: None.
118 118
119 119 notify.merge
120 120 If True, send notifications for merge changesets. Default: True.
121 121
122 122 notify.mbox
123 123 If set, append mails to this mbox file instead of sending. Default: None.
124 124
125 125 notify.fromauthor
126 126 If set, use the committer of the first changeset in a changegroup for
127 127 the "From" field of the notification mail. If not set, take the user
128 128 from the pushing repo. Default: False.
129 129
130 130 If set, the following entries will also be used to customize the
131 131 notifications:
132 132
133 133 email.from
134 134 Email ``From`` address to use if none can be found in the generated
135 135 email content.
136 136
137 137 web.baseurl
138 138 Root repository URL to combine with repository paths when making
139 139 references. See also ``notify.strip``.
140 140
141 141 '''
142 142 from __future__ import absolute_import
143 143
144 144 import email
145 145 import email.parser as emailparser
146 146 import fnmatch
147 147 import socket
148 148 import time
149 149
150 150 from mercurial.i18n import _
151 151 from mercurial import (
152 152 error,
153 153 logcmdutil,
154 154 mail,
155 155 patch,
156 pycompat,
156 157 registrar,
157 158 util,
158 159 )
159 160 from mercurial.utils import (
160 161 dateutil,
161 162 stringutil,
162 163 )
163 164
165 if pycompat.ispy3:
166 import email.errors as emailerrors
167 else:
168 emailerrors = email.Errors
169
164 170 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
165 171 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
166 172 # be specifying the version(s) of Mercurial they are tested with, or
167 173 # leave the attribute unspecified.
168 174 testedwith = 'ships-with-hg-core'
169 175
170 176 configtable = {}
171 177 configitem = registrar.configitem(configtable)
172 178
173 179 configitem('notify', 'changegroup',
174 180 default=None,
175 181 )
176 182 configitem('notify', 'config',
177 183 default=None,
178 184 )
179 185 configitem('notify', 'diffstat',
180 186 default=True,
181 187 )
182 188 configitem('notify', 'domain',
183 189 default=None,
184 190 )
185 191 configitem('notify', 'fromauthor',
186 192 default=None,
187 193 )
188 194 configitem('notify', 'incoming',
189 195 default=None,
190 196 )
191 197 configitem('notify', 'maxdiff',
192 198 default=300,
193 199 )
194 200 configitem('notify', 'maxdiffstat',
195 201 default=-1,
196 202 )
197 203 configitem('notify', 'maxsubject',
198 204 default=67,
199 205 )
200 206 configitem('notify', 'mbox',
201 207 default=None,
202 208 )
203 209 configitem('notify', 'merge',
204 210 default=True,
205 211 )
206 212 configitem('notify', 'outgoing',
207 213 default=None,
208 214 )
209 215 configitem('notify', 'sources',
210 216 default='serve',
211 217 )
212 218 configitem('notify', 'showfunc',
213 219 default=None,
214 220 )
215 221 configitem('notify', 'strip',
216 222 default=0,
217 223 )
218 224 configitem('notify', 'style',
219 225 default=None,
220 226 )
221 227 configitem('notify', 'template',
222 228 default=None,
223 229 )
224 230 configitem('notify', 'test',
225 231 default=True,
226 232 )
227 233
228 234 # template for single changeset can include email headers.
229 235 single_template = b'''
230 236 Subject: changeset in {webroot}: {desc|firstline|strip}
231 237 From: {author}
232 238
233 239 changeset {node|short} in {root}
234 240 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
235 241 description:
236 242 \t{desc|tabindent|strip}
237 243 '''.lstrip()
238 244
239 245 # template for multiple changesets should not contain email headers,
240 246 # because only first set of headers will be used and result will look
241 247 # strange.
242 248 multiple_template = b'''
243 249 changeset {node|short} in {root}
244 250 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
245 251 summary: {desc|firstline}
246 252 '''
247 253
248 254 deftemplates = {
249 255 'changegroup': multiple_template,
250 256 }
251 257
252 258 class notifier(object):
253 259 '''email notification class.'''
254 260
255 261 def __init__(self, ui, repo, hooktype):
256 262 self.ui = ui
257 263 cfg = self.ui.config('notify', 'config')
258 264 if cfg:
259 265 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
260 266 self.repo = repo
261 267 self.stripcount = int(self.ui.config('notify', 'strip'))
262 268 self.root = self.strip(self.repo.root)
263 269 self.domain = self.ui.config('notify', 'domain')
264 270 self.mbox = self.ui.config('notify', 'mbox')
265 271 self.test = self.ui.configbool('notify', 'test')
266 272 self.charsets = mail._charsets(self.ui)
267 273 self.subs = self.subscribers()
268 274 self.merge = self.ui.configbool('notify', 'merge')
269 275 self.showfunc = self.ui.configbool('notify', 'showfunc')
270 276 if self.showfunc is None:
271 277 self.showfunc = self.ui.configbool('diff', 'showfunc')
272 278
273 279 mapfile = None
274 280 template = (self.ui.config('notify', hooktype) or
275 281 self.ui.config('notify', 'template'))
276 282 if not template:
277 283 mapfile = self.ui.config('notify', 'style')
278 284 if not mapfile and not template:
279 285 template = deftemplates.get(hooktype) or single_template
280 286 spec = logcmdutil.templatespec(template, mapfile)
281 287 self.t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
282 288
283 289 def strip(self, path):
284 290 '''strip leading slashes from local path, turn into web-safe path.'''
285 291
286 292 path = util.pconvert(path)
287 293 count = self.stripcount
288 294 while count > 0:
289 295 c = path.find('/')
290 296 if c == -1:
291 297 break
292 298 path = path[c + 1:]
293 299 count -= 1
294 300 return path
295 301
296 302 def fixmail(self, addr):
297 303 '''try to clean up email addresses.'''
298 304
299 305 addr = stringutil.email(addr.strip())
300 306 if self.domain:
301 307 a = addr.find('@localhost')
302 308 if a != -1:
303 309 addr = addr[:a]
304 310 if '@' not in addr:
305 311 return addr + '@' + self.domain
306 312 return addr
307 313
308 314 def subscribers(self):
309 315 '''return list of email addresses of subscribers to this repo.'''
310 316 subs = set()
311 317 for user, pats in self.ui.configitems('usersubs'):
312 318 for pat in pats.split(','):
313 319 if '#' in pat:
314 320 pat, revs = pat.split('#', 1)
315 321 else:
316 322 revs = None
317 323 if fnmatch.fnmatch(self.repo.root, pat.strip()):
318 324 subs.add((self.fixmail(user), revs))
319 325 for pat, users in self.ui.configitems('reposubs'):
320 326 if '#' in pat:
321 327 pat, revs = pat.split('#', 1)
322 328 else:
323 329 revs = None
324 330 if fnmatch.fnmatch(self.repo.root, pat):
325 331 for user in users.split(','):
326 332 subs.add((self.fixmail(user), revs))
327 333 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
328 334 for s, r in sorted(subs)]
329 335
330 336 def node(self, ctx, **props):
331 337 '''format one changeset, unless it is a suppressed merge.'''
332 338 if not self.merge and len(ctx.parents()) > 1:
333 339 return False
334 340 self.t.show(ctx, changes=ctx.changeset(),
335 341 baseurl=self.ui.config('web', 'baseurl'),
336 342 root=self.repo.root, webroot=self.root, **props)
337 343 return True
338 344
339 345 def skipsource(self, source):
340 346 '''true if incoming changes from this source should be skipped.'''
341 347 ok_sources = self.ui.config('notify', 'sources').split()
342 348 return source not in ok_sources
343 349
344 350 def send(self, ctx, count, data):
345 351 '''send message.'''
346 352
347 353 # Select subscribers by revset
348 354 subs = set()
349 355 for sub, spec in self.subs:
350 356 if spec is None:
351 357 subs.add(sub)
352 358 continue
353 359 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
354 360 if len(revs):
355 361 subs.add(sub)
356 362 continue
357 363 if len(subs) == 0:
358 364 self.ui.debug('notify: no subscribers to selected repo '
359 365 'and revset\n')
360 366 return
361 367
362 368 p = emailparser.Parser()
363 369 try:
364 370 msg = p.parsestr(data)
365 except email.Errors.MessageParseError as inst:
371 except emailerrors.MessageParseError as inst:
366 372 raise error.Abort(inst)
367 373
368 374 # store sender and subject
369 375 sender, subject = msg['From'], msg['Subject']
370 376 del msg['From'], msg['Subject']
371 377
372 378 if not msg.is_multipart():
373 379 # create fresh mime message from scratch
374 380 # (multipart templates must take care of this themselves)
375 381 headers = msg.items()
376 382 payload = msg.get_payload()
377 383 # for notification prefer readability over data precision
378 384 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
379 385 # reinstate custom headers
380 386 for k, v in headers:
381 387 msg[k] = v
382 388
383 389 msg['Date'] = dateutil.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
384 390
385 391 # try to make subject line exist and be useful
386 392 if not subject:
387 393 if count > 1:
388 394 subject = _('%s: %d new changesets') % (self.root, count)
389 395 else:
390 396 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
391 397 subject = '%s: %s' % (self.root, s)
392 398 maxsubject = int(self.ui.config('notify', 'maxsubject'))
393 399 if maxsubject:
394 400 subject = stringutil.ellipsis(subject, maxsubject)
395 401 msg['Subject'] = mail.headencode(self.ui, subject,
396 402 self.charsets, self.test)
397 403
398 404 # try to make message have proper sender
399 405 if not sender:
400 406 sender = self.ui.config('email', 'from') or self.ui.username()
401 407 if '@' not in sender or '@localhost' in sender:
402 408 sender = self.fixmail(sender)
403 409 msg['From'] = mail.addressencode(self.ui, sender,
404 410 self.charsets, self.test)
405 411
406 412 msg['X-Hg-Notification'] = 'changeset %s' % ctx
407 413 if not msg['Message-Id']:
408 414 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
409 415 (ctx, int(time.time()),
410 416 hash(self.repo.root), socket.getfqdn()))
411 417 msg['To'] = ', '.join(sorted(subs))
412 418
413 419 msgtext = msg.as_string()
414 420 if self.test:
415 421 self.ui.write(msgtext)
416 422 if not msgtext.endswith('\n'):
417 423 self.ui.write('\n')
418 424 else:
419 425 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
420 426 (len(subs), count))
421 427 mail.sendmail(self.ui, stringutil.email(msg['From']),
422 428 subs, msgtext, mbox=self.mbox)
423 429
424 430 def diff(self, ctx, ref=None):
425 431
426 432 maxdiff = int(self.ui.config('notify', 'maxdiff'))
427 433 prev = ctx.p1().node()
428 434 if ref:
429 435 ref = ref.node()
430 436 else:
431 437 ref = ctx.node()
432 438 diffopts = patch.diffallopts(self.ui)
433 439 diffopts.showfunc = self.showfunc
434 440 chunks = patch.diff(self.repo, prev, ref, opts=diffopts)
435 441 difflines = ''.join(chunks).splitlines()
436 442
437 443 if self.ui.configbool('notify', 'diffstat'):
438 444 maxdiffstat = int(self.ui.config('notify', 'maxdiffstat'))
439 445 s = patch.diffstat(difflines)
440 446 # s may be nil, don't include the header if it is
441 447 if s:
442 448 if maxdiffstat >= 0 and s.count("\n") > maxdiffstat + 1:
443 449 s = s.split("\n")
444 450 msg = _('\ndiffstat (truncated from %d to %d lines):\n\n')
445 451 self.ui.write(msg % (len(s) - 2, maxdiffstat))
446 452 self.ui.write("\n".join(s[:maxdiffstat] + s[-2:]))
447 453 else:
448 454 self.ui.write(_('\ndiffstat:\n\n%s') % s)
449 455
450 456 if maxdiff == 0:
451 457 return
452 458 elif maxdiff > 0 and len(difflines) > maxdiff:
453 459 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
454 460 self.ui.write(msg % (len(difflines), maxdiff))
455 461 difflines = difflines[:maxdiff]
456 462 elif difflines:
457 463 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
458 464
459 465 self.ui.write("\n".join(difflines))
460 466
461 467 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
462 468 '''send email notifications to interested subscribers.
463 469
464 470 if used as changegroup hook, send one email for all changesets in
465 471 changegroup. else send one email per changeset.'''
466 472
467 473 n = notifier(ui, repo, hooktype)
468 474 ctx = repo.unfiltered()[node]
469 475
470 476 if not n.subs:
471 477 ui.debug('notify: no subscribers to repository %s\n' % n.root)
472 478 return
473 479 if n.skipsource(source):
474 480 ui.debug('notify: changes have source "%s" - skipping\n' % source)
475 481 return
476 482
477 483 ui.pushbuffer()
478 484 data = ''
479 485 count = 0
480 486 author = ''
481 487 if hooktype == 'changegroup' or hooktype == 'outgoing':
482 488 for rev in repo.changelog.revs(start=ctx.rev()):
483 489 if n.node(repo[rev]):
484 490 count += 1
485 491 if not author:
486 492 author = repo[rev].user()
487 493 else:
488 494 data += ui.popbuffer()
489 495 ui.note(_('notify: suppressing notification for merge %d:%s\n')
490 496 % (rev, repo[rev].hex()[:12]))
491 497 ui.pushbuffer()
492 498 if count:
493 499 n.diff(ctx, repo['tip'])
494 500 elif ctx.rev() in repo:
495 501 if not n.node(ctx):
496 502 ui.popbuffer()
497 503 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
498 504 (ctx.rev(), ctx.hex()[:12]))
499 505 return
500 506 count += 1
501 507 n.diff(ctx)
502 508 if not author:
503 509 author = ctx.user()
504 510
505 511 data += ui.popbuffer()
506 512 fromauthor = ui.config('notify', 'fromauthor')
507 513 if author and fromauthor:
508 514 data = '\n'.join(['From: %s' % author, data])
509 515
510 516 if count:
511 517 n.send(ctx, count, data)
General Comments 0
You need to be logged in to leave comments. Login now