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