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