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