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