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