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