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