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