##// END OF EJS Templates
notify: use absolute_import
timeless -
r28416:d3c8183f default
parent child Browse files
Show More
@@ -1,418 +1,428 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 from __future__ import absolute_import
135 136
136 import email, socket, time
137 import email
138 import fnmatch
139 import socket
140 import time
141
142 from mercurial import (
143 cmdutil,
144 error,
145 mail,
146 patch,
147 util,
148 )
137 149 from mercurial.i18n import _
138 from mercurial import patch, cmdutil, util, mail, error
139 import fnmatch
140 150
141 151 # Note for extension authors: ONLY specify testedwith = 'internal' for
142 152 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
143 153 # be specifying the version(s) of Mercurial they are tested with, or
144 154 # leave the attribute unspecified.
145 155 testedwith = 'internal'
146 156
147 157 # template for single changeset can include email headers.
148 158 single_template = '''
149 159 Subject: changeset in {webroot}: {desc|firstline|strip}
150 160 From: {author}
151 161
152 162 changeset {node|short} in {root}
153 163 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
154 164 description:
155 165 \t{desc|tabindent|strip}
156 166 '''.lstrip()
157 167
158 168 # template for multiple changesets should not contain email headers,
159 169 # because only first set of headers will be used and result will look
160 170 # strange.
161 171 multiple_template = '''
162 172 changeset {node|short} in {root}
163 173 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
164 174 summary: {desc|firstline}
165 175 '''
166 176
167 177 deftemplates = {
168 178 'changegroup': multiple_template,
169 179 }
170 180
171 181 class notifier(object):
172 182 '''email notification class.'''
173 183
174 184 def __init__(self, ui, repo, hooktype):
175 185 self.ui = ui
176 186 cfg = self.ui.config('notify', 'config')
177 187 if cfg:
178 188 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
179 189 self.repo = repo
180 190 self.stripcount = int(self.ui.config('notify', 'strip', 0))
181 191 self.root = self.strip(self.repo.root)
182 192 self.domain = self.ui.config('notify', 'domain')
183 193 self.mbox = self.ui.config('notify', 'mbox')
184 194 self.test = self.ui.configbool('notify', 'test', True)
185 195 self.charsets = mail._charsets(self.ui)
186 196 self.subs = self.subscribers()
187 197 self.merge = self.ui.configbool('notify', 'merge', True)
188 198
189 199 mapfile = self.ui.config('notify', 'style')
190 200 template = (self.ui.config('notify', hooktype) or
191 201 self.ui.config('notify', 'template'))
192 202 if not mapfile and not template:
193 203 template = deftemplates.get(hooktype) or single_template
194 204 self.t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
195 205 template, mapfile, False)
196 206
197 207 def strip(self, path):
198 208 '''strip leading slashes from local path, turn into web-safe path.'''
199 209
200 210 path = util.pconvert(path)
201 211 count = self.stripcount
202 212 while count > 0:
203 213 c = path.find('/')
204 214 if c == -1:
205 215 break
206 216 path = path[c + 1:]
207 217 count -= 1
208 218 return path
209 219
210 220 def fixmail(self, addr):
211 221 '''try to clean up email addresses.'''
212 222
213 223 addr = util.email(addr.strip())
214 224 if self.domain:
215 225 a = addr.find('@localhost')
216 226 if a != -1:
217 227 addr = addr[:a]
218 228 if '@' not in addr:
219 229 return addr + '@' + self.domain
220 230 return addr
221 231
222 232 def subscribers(self):
223 233 '''return list of email addresses of subscribers to this repo.'''
224 234 subs = set()
225 235 for user, pats in self.ui.configitems('usersubs'):
226 236 for pat in pats.split(','):
227 237 if '#' in pat:
228 238 pat, revs = pat.split('#', 1)
229 239 else:
230 240 revs = None
231 241 if fnmatch.fnmatch(self.repo.root, pat.strip()):
232 242 subs.add((self.fixmail(user), revs))
233 243 for pat, users in self.ui.configitems('reposubs'):
234 244 if '#' in pat:
235 245 pat, revs = pat.split('#', 1)
236 246 else:
237 247 revs = None
238 248 if fnmatch.fnmatch(self.repo.root, pat):
239 249 for user in users.split(','):
240 250 subs.add((self.fixmail(user), revs))
241 251 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
242 252 for s, r in sorted(subs)]
243 253
244 254 def node(self, ctx, **props):
245 255 '''format one changeset, unless it is a suppressed merge.'''
246 256 if not self.merge and len(ctx.parents()) > 1:
247 257 return False
248 258 self.t.show(ctx, changes=ctx.changeset(),
249 259 baseurl=self.ui.config('web', 'baseurl'),
250 260 root=self.repo.root, webroot=self.root, **props)
251 261 return True
252 262
253 263 def skipsource(self, source):
254 264 '''true if incoming changes from this source should be skipped.'''
255 265 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
256 266 return source not in ok_sources
257 267
258 268 def send(self, ctx, count, data):
259 269 '''send message.'''
260 270
261 271 # Select subscribers by revset
262 272 subs = set()
263 273 for sub, spec in self.subs:
264 274 if spec is None:
265 275 subs.add(sub)
266 276 continue
267 277 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
268 278 if len(revs):
269 279 subs.add(sub)
270 280 continue
271 281 if len(subs) == 0:
272 282 self.ui.debug('notify: no subscribers to selected repo '
273 283 'and revset\n')
274 284 return
275 285
276 286 p = email.Parser.Parser()
277 287 try:
278 288 msg = p.parsestr(data)
279 289 except email.Errors.MessageParseError as inst:
280 290 raise error.Abort(inst)
281 291
282 292 # store sender and subject
283 293 sender, subject = msg['From'], msg['Subject']
284 294 del msg['From'], msg['Subject']
285 295
286 296 if not msg.is_multipart():
287 297 # create fresh mime message from scratch
288 298 # (multipart templates must take care of this themselves)
289 299 headers = msg.items()
290 300 payload = msg.get_payload()
291 301 # for notification prefer readability over data precision
292 302 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
293 303 # reinstate custom headers
294 304 for k, v in headers:
295 305 msg[k] = v
296 306
297 307 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
298 308
299 309 # try to make subject line exist and be useful
300 310 if not subject:
301 311 if count > 1:
302 312 subject = _('%s: %d new changesets') % (self.root, count)
303 313 else:
304 314 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
305 315 subject = '%s: %s' % (self.root, s)
306 316 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
307 317 if maxsubject:
308 318 subject = util.ellipsis(subject, maxsubject)
309 319 msg['Subject'] = mail.headencode(self.ui, subject,
310 320 self.charsets, self.test)
311 321
312 322 # try to make message have proper sender
313 323 if not sender:
314 324 sender = self.ui.config('email', 'from') or self.ui.username()
315 325 if '@' not in sender or '@localhost' in sender:
316 326 sender = self.fixmail(sender)
317 327 msg['From'] = mail.addressencode(self.ui, sender,
318 328 self.charsets, self.test)
319 329
320 330 msg['X-Hg-Notification'] = 'changeset %s' % ctx
321 331 if not msg['Message-Id']:
322 332 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
323 333 (ctx, int(time.time()),
324 334 hash(self.repo.root), socket.getfqdn()))
325 335 msg['To'] = ', '.join(sorted(subs))
326 336
327 337 msgtext = msg.as_string()
328 338 if self.test:
329 339 self.ui.write(msgtext)
330 340 if not msgtext.endswith('\n'):
331 341 self.ui.write('\n')
332 342 else:
333 343 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
334 344 (len(subs), count))
335 345 mail.sendmail(self.ui, util.email(msg['From']),
336 346 subs, msgtext, mbox=self.mbox)
337 347
338 348 def diff(self, ctx, ref=None):
339 349
340 350 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
341 351 prev = ctx.p1().node()
342 352 if ref:
343 353 ref = ref.node()
344 354 else:
345 355 ref = ctx.node()
346 356 chunks = patch.diff(self.repo, prev, ref,
347 357 opts=patch.diffallopts(self.ui))
348 358 difflines = ''.join(chunks).splitlines()
349 359
350 360 if self.ui.configbool('notify', 'diffstat', True):
351 361 s = patch.diffstat(difflines)
352 362 # s may be nil, don't include the header if it is
353 363 if s:
354 364 self.ui.write('\ndiffstat:\n\n%s' % s)
355 365
356 366 if maxdiff == 0:
357 367 return
358 368 elif maxdiff > 0 and len(difflines) > maxdiff:
359 369 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
360 370 self.ui.write(msg % (len(difflines), maxdiff))
361 371 difflines = difflines[:maxdiff]
362 372 elif difflines:
363 373 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
364 374
365 375 self.ui.write("\n".join(difflines))
366 376
367 377 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
368 378 '''send email notifications to interested subscribers.
369 379
370 380 if used as changegroup hook, send one email for all changesets in
371 381 changegroup. else send one email per changeset.'''
372 382
373 383 n = notifier(ui, repo, hooktype)
374 384 ctx = repo[node]
375 385
376 386 if not n.subs:
377 387 ui.debug('notify: no subscribers to repository %s\n' % n.root)
378 388 return
379 389 if n.skipsource(source):
380 390 ui.debug('notify: changes have source "%s" - skipping\n' % source)
381 391 return
382 392
383 393 ui.pushbuffer()
384 394 data = ''
385 395 count = 0
386 396 author = ''
387 397 if hooktype == 'changegroup' or hooktype == 'outgoing':
388 398 start, end = ctx.rev(), len(repo)
389 399 for rev in xrange(start, end):
390 400 if n.node(repo[rev]):
391 401 count += 1
392 402 if not author:
393 403 author = repo[rev].user()
394 404 else:
395 405 data += ui.popbuffer()
396 406 ui.note(_('notify: suppressing notification for merge %d:%s\n')
397 407 % (rev, repo[rev].hex()[:12]))
398 408 ui.pushbuffer()
399 409 if count:
400 410 n.diff(ctx, repo['tip'])
401 411 else:
402 412 if not n.node(ctx):
403 413 ui.popbuffer()
404 414 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
405 415 (ctx.rev(), ctx.hex()[:12]))
406 416 return
407 417 count += 1
408 418 n.diff(ctx)
409 419 if not author:
410 420 author = ctx.user()
411 421
412 422 data += ui.popbuffer()
413 423 fromauthor = ui.config('notify', 'fromauthor')
414 424 if author and fromauthor:
415 425 data = '\n'.join(['From: %s' % author, data])
416 426
417 427 if count:
418 428 n.send(ctx, count, data)
@@ -1,131 +1,130 b''
1 1 #require test-repo
2 2
3 3 $ cd "$TESTDIR"/..
4 4
5 5 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
6 6 contrib/check-code.py not using absolute_import
7 7 contrib/check-code.py requires print_function
8 8 contrib/debugshell.py not using absolute_import
9 9 contrib/import-checker.py not using absolute_import
10 10 contrib/import-checker.py requires print_function
11 11 contrib/memory.py not using absolute_import
12 12 contrib/perf.py not using absolute_import
13 13 contrib/python-hook-examples.py not using absolute_import
14 14 contrib/revsetbenchmarks.py not using absolute_import
15 15 contrib/revsetbenchmarks.py requires print_function
16 16 contrib/showstack.py not using absolute_import
17 17 contrib/synthrepo.py not using absolute_import
18 18 contrib/win32/hgwebdir_wsgi.py not using absolute_import
19 19 doc/check-seclevel.py not using absolute_import
20 20 doc/gendoc.py not using absolute_import
21 21 doc/hgmanpage.py not using absolute_import
22 22 hgext/__init__.py not using absolute_import
23 23 hgext/color.py not using absolute_import
24 24 hgext/eol.py not using absolute_import
25 25 hgext/extdiff.py not using absolute_import
26 26 hgext/factotum.py not using absolute_import
27 27 hgext/fetch.py not using absolute_import
28 28 hgext/gpg.py not using absolute_import
29 29 hgext/graphlog.py not using absolute_import
30 30 hgext/hgcia.py not using absolute_import
31 31 hgext/hgk.py not using absolute_import
32 32 hgext/highlight/__init__.py not using absolute_import
33 33 hgext/highlight/highlight.py not using absolute_import
34 34 hgext/histedit.py not using absolute_import
35 35 hgext/largefiles/__init__.py not using absolute_import
36 36 hgext/largefiles/basestore.py not using absolute_import
37 37 hgext/largefiles/lfcommands.py not using absolute_import
38 38 hgext/largefiles/lfutil.py not using absolute_import
39 39 hgext/largefiles/localstore.py not using absolute_import
40 40 hgext/largefiles/overrides.py not using absolute_import
41 41 hgext/largefiles/proto.py not using absolute_import
42 42 hgext/largefiles/remotestore.py not using absolute_import
43 43 hgext/largefiles/reposetup.py not using absolute_import
44 44 hgext/largefiles/uisetup.py not using absolute_import
45 45 hgext/largefiles/wirestore.py not using absolute_import
46 46 hgext/mq.py not using absolute_import
47 hgext/notify.py not using absolute_import
48 47 hgext/rebase.py not using absolute_import
49 48 hgext/share.py not using absolute_import
50 49 hgext/transplant.py not using absolute_import
51 50 hgext/win32mbcs.py not using absolute_import
52 51 hgext/win32text.py not using absolute_import
53 52 i18n/check-translation.py not using absolute_import
54 53 i18n/polib.py not using absolute_import
55 54 setup.py not using absolute_import
56 55 tests/filterpyflakes.py requires print_function
57 56 tests/generate-working-copy-states.py requires print_function
58 57 tests/get-with-headers.py requires print_function
59 58 tests/heredoctest.py requires print_function
60 59 tests/hypothesishelpers.py not using absolute_import
61 60 tests/hypothesishelpers.py requires print_function
62 61 tests/killdaemons.py not using absolute_import
63 62 tests/md5sum.py not using absolute_import
64 63 tests/mockblackbox.py not using absolute_import
65 64 tests/printenv.py not using absolute_import
66 65 tests/readlink.py not using absolute_import
67 66 tests/readlink.py requires print_function
68 67 tests/revlog-formatv0.py not using absolute_import
69 68 tests/run-tests.py not using absolute_import
70 69 tests/seq.py not using absolute_import
71 70 tests/seq.py requires print_function
72 71 tests/silenttestrunner.py not using absolute_import
73 72 tests/silenttestrunner.py requires print_function
74 73 tests/sitecustomize.py not using absolute_import
75 74 tests/svn-safe-append.py not using absolute_import
76 75 tests/svnxml.py not using absolute_import
77 76 tests/test-ancestor.py requires print_function
78 77 tests/test-atomictempfile.py not using absolute_import
79 78 tests/test-batching.py not using absolute_import
80 79 tests/test-batching.py requires print_function
81 80 tests/test-bdiff.py not using absolute_import
82 81 tests/test-bdiff.py requires print_function
83 82 tests/test-context.py not using absolute_import
84 83 tests/test-context.py requires print_function
85 84 tests/test-demandimport.py not using absolute_import
86 85 tests/test-demandimport.py requires print_function
87 86 tests/test-doctest.py not using absolute_import
88 87 tests/test-duplicateoptions.py not using absolute_import
89 88 tests/test-duplicateoptions.py requires print_function
90 89 tests/test-filecache.py not using absolute_import
91 90 tests/test-filecache.py requires print_function
92 91 tests/test-filelog.py not using absolute_import
93 92 tests/test-filelog.py requires print_function
94 93 tests/test-hg-parseurl.py not using absolute_import
95 94 tests/test-hg-parseurl.py requires print_function
96 95 tests/test-hgweb-auth.py not using absolute_import
97 96 tests/test-hgweb-auth.py requires print_function
98 97 tests/test-hgwebdir-paths.py not using absolute_import
99 98 tests/test-hybridencode.py not using absolute_import
100 99 tests/test-hybridencode.py requires print_function
101 100 tests/test-lrucachedict.py not using absolute_import
102 101 tests/test-lrucachedict.py requires print_function
103 102 tests/test-manifest.py not using absolute_import
104 103 tests/test-minirst.py not using absolute_import
105 104 tests/test-minirst.py requires print_function
106 105 tests/test-parseindex2.py not using absolute_import
107 106 tests/test-parseindex2.py requires print_function
108 107 tests/test-pathencode.py not using absolute_import
109 108 tests/test-pathencode.py requires print_function
110 109 tests/test-propertycache.py not using absolute_import
111 110 tests/test-propertycache.py requires print_function
112 111 tests/test-revlog-ancestry.py not using absolute_import
113 112 tests/test-revlog-ancestry.py requires print_function
114 113 tests/test-run-tests.py not using absolute_import
115 114 tests/test-simplemerge.py not using absolute_import
116 115 tests/test-status-inprocess.py not using absolute_import
117 116 tests/test-status-inprocess.py requires print_function
118 117 tests/test-symlink-os-yes-fs-no.py not using absolute_import
119 118 tests/test-trusted.py not using absolute_import
120 119 tests/test-trusted.py requires print_function
121 120 tests/test-ui-color.py not using absolute_import
122 121 tests/test-ui-color.py requires print_function
123 122 tests/test-ui-config.py not using absolute_import
124 123 tests/test-ui-config.py requires print_function
125 124 tests/test-ui-verbosity.py not using absolute_import
126 125 tests/test-ui-verbosity.py requires print_function
127 126 tests/test-url.py not using absolute_import
128 127 tests/test-url.py requires print_function
129 128 tests/test-walkrepo.py requires print_function
130 129 tests/test-wireproto.py requires print_function
131 130 tests/tinyproxy.py requires print_function
General Comments 0
You need to be logged in to leave comments. Login now