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