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