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