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