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