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