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