##// END OF EJS Templates
notify: adapt to new location of email module's errors...
Augie Fackler -
r40320:46b55f27 default
parent child Browse files
Show More
@@ -1,511 +1,517 b''
1 # notify.py - email notifications for mercurial
1 # notify.py - email notifications for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''hooks for sending email push notifications
8 '''hooks for sending email push notifications
9
9
10 This extension implements hooks to send email notifications when
10 This extension implements hooks to send email notifications when
11 changesets are sent from or received by the local repository.
11 changesets are sent from or received by the local repository.
12
12
13 First, enable the extension as explained in :hg:`help extensions`, and
13 First, enable the extension as explained in :hg:`help extensions`, and
14 register the hook you want to run. ``incoming`` and ``changegroup`` hooks
14 register the hook you want to run. ``incoming`` and ``changegroup`` hooks
15 are run when changesets are received, while ``outgoing`` hooks are for
15 are run when changesets are received, while ``outgoing`` hooks are for
16 changesets sent to another repository::
16 changesets sent to another repository::
17
17
18 [hooks]
18 [hooks]
19 # one email for each incoming changeset
19 # one email for each incoming changeset
20 incoming.notify = python:hgext.notify.hook
20 incoming.notify = python:hgext.notify.hook
21 # one email for all incoming changesets
21 # one email for all incoming changesets
22 changegroup.notify = python:hgext.notify.hook
22 changegroup.notify = python:hgext.notify.hook
23
23
24 # one email for all outgoing changesets
24 # one email for all outgoing changesets
25 outgoing.notify = python:hgext.notify.hook
25 outgoing.notify = python:hgext.notify.hook
26
26
27 This registers the hooks. To enable notification, subscribers must
27 This registers the hooks. To enable notification, subscribers must
28 be assigned to repositories. The ``[usersubs]`` section maps multiple
28 be assigned to repositories. The ``[usersubs]`` section maps multiple
29 repositories to a given recipient. The ``[reposubs]`` section maps
29 repositories to a given recipient. The ``[reposubs]`` section maps
30 multiple recipients to a single repository::
30 multiple recipients to a single repository::
31
31
32 [usersubs]
32 [usersubs]
33 # key is subscriber email, value is a comma-separated list of repo patterns
33 # key is subscriber email, value is a comma-separated list of repo patterns
34 user@host = pattern
34 user@host = pattern
35
35
36 [reposubs]
36 [reposubs]
37 # key is repo pattern, value is a comma-separated list of subscriber emails
37 # key is repo pattern, value is a comma-separated list of subscriber emails
38 pattern = user@host
38 pattern = user@host
39
39
40 A ``pattern`` is a ``glob`` matching the absolute path to a repository,
40 A ``pattern`` is a ``glob`` matching the absolute path to a repository,
41 optionally combined with a revset expression. A revset expression, if
41 optionally combined with a revset expression. A revset expression, if
42 present, is separated from the glob by a hash. Example::
42 present, is separated from the glob by a hash. Example::
43
43
44 [reposubs]
44 [reposubs]
45 */widgets#branch(release) = qa-team@example.com
45 */widgets#branch(release) = qa-team@example.com
46
46
47 This sends to ``qa-team@example.com`` whenever a changeset on the ``release``
47 This sends to ``qa-team@example.com`` whenever a changeset on the ``release``
48 branch triggers a notification in any repository ending in ``widgets``.
48 branch triggers a notification in any repository ending in ``widgets``.
49
49
50 In order to place them under direct user management, ``[usersubs]`` and
50 In order to place them under direct user management, ``[usersubs]`` and
51 ``[reposubs]`` sections may be placed in a separate ``hgrc`` file and
51 ``[reposubs]`` sections may be placed in a separate ``hgrc`` file and
52 incorporated by reference::
52 incorporated by reference::
53
53
54 [notify]
54 [notify]
55 config = /path/to/subscriptionsfile
55 config = /path/to/subscriptionsfile
56
56
57 Notifications will not be sent until the ``notify.test`` value is set
57 Notifications will not be sent until the ``notify.test`` value is set
58 to ``False``; see below.
58 to ``False``; see below.
59
59
60 Notifications content can be tweaked with the following configuration entries:
60 Notifications content can be tweaked with the following configuration entries:
61
61
62 notify.test
62 notify.test
63 If ``True``, print messages to stdout instead of sending them. Default: True.
63 If ``True``, print messages to stdout instead of sending them. Default: True.
64
64
65 notify.sources
65 notify.sources
66 Space-separated list of change sources. Notifications are activated only
66 Space-separated list of change sources. Notifications are activated only
67 when a changeset's source is in this list. Sources may be:
67 when a changeset's source is in this list. Sources may be:
68
68
69 :``serve``: changesets received via http or ssh
69 :``serve``: changesets received via http or ssh
70 :``pull``: changesets received via ``hg pull``
70 :``pull``: changesets received via ``hg pull``
71 :``unbundle``: changesets received via ``hg unbundle``
71 :``unbundle``: changesets received via ``hg unbundle``
72 :``push``: changesets sent or received via ``hg push``
72 :``push``: changesets sent or received via ``hg push``
73 :``bundle``: changesets sent via ``hg unbundle``
73 :``bundle``: changesets sent via ``hg unbundle``
74
74
75 Default: serve.
75 Default: serve.
76
76
77 notify.strip
77 notify.strip
78 Number of leading slashes to strip from url paths. By default, notifications
78 Number of leading slashes to strip from url paths. By default, notifications
79 reference repositories with their absolute path. ``notify.strip`` lets you
79 reference repositories with their absolute path. ``notify.strip`` lets you
80 turn them into relative paths. For example, ``notify.strip=3`` will change
80 turn them into relative paths. For example, ``notify.strip=3`` will change
81 ``/long/path/repository`` into ``repository``. Default: 0.
81 ``/long/path/repository`` into ``repository``. Default: 0.
82
82
83 notify.domain
83 notify.domain
84 Default email domain for sender or recipients with no explicit domain.
84 Default email domain for sender or recipients with no explicit domain.
85
85
86 notify.style
86 notify.style
87 Style file to use when formatting emails.
87 Style file to use when formatting emails.
88
88
89 notify.template
89 notify.template
90 Template to use when formatting emails.
90 Template to use when formatting emails.
91
91
92 notify.incoming
92 notify.incoming
93 Template to use when run as an incoming hook, overriding ``notify.template``.
93 Template to use when run as an incoming hook, overriding ``notify.template``.
94
94
95 notify.outgoing
95 notify.outgoing
96 Template to use when run as an outgoing hook, overriding ``notify.template``.
96 Template to use when run as an outgoing hook, overriding ``notify.template``.
97
97
98 notify.changegroup
98 notify.changegroup
99 Template to use when running as a changegroup hook, overriding
99 Template to use when running as a changegroup hook, overriding
100 ``notify.template``.
100 ``notify.template``.
101
101
102 notify.maxdiff
102 notify.maxdiff
103 Maximum number of diff lines to include in notification email. Set to 0
103 Maximum number of diff lines to include in notification email. Set to 0
104 to disable the diff, or -1 to include all of it. Default: 300.
104 to disable the diff, or -1 to include all of it. Default: 300.
105
105
106 notify.maxdiffstat
106 notify.maxdiffstat
107 Maximum number of diffstat lines to include in notification email. Set to -1
107 Maximum number of diffstat lines to include in notification email. Set to -1
108 to include all of it. Default: -1.
108 to include all of it. Default: -1.
109
109
110 notify.maxsubject
110 notify.maxsubject
111 Maximum number of characters in email's subject line. Default: 67.
111 Maximum number of characters in email's subject line. Default: 67.
112
112
113 notify.diffstat
113 notify.diffstat
114 Set to True to include a diffstat before diff content. Default: True.
114 Set to True to include a diffstat before diff content. Default: True.
115
115
116 notify.showfunc
116 notify.showfunc
117 If set, override ``diff.showfunc`` for the diff content. Default: None.
117 If set, override ``diff.showfunc`` for the diff content. Default: None.
118
118
119 notify.merge
119 notify.merge
120 If True, send notifications for merge changesets. Default: True.
120 If True, send notifications for merge changesets. Default: True.
121
121
122 notify.mbox
122 notify.mbox
123 If set, append mails to this mbox file instead of sending. Default: None.
123 If set, append mails to this mbox file instead of sending. Default: None.
124
124
125 notify.fromauthor
125 notify.fromauthor
126 If set, use the committer of the first changeset in a changegroup for
126 If set, use the committer of the first changeset in a changegroup for
127 the "From" field of the notification mail. If not set, take the user
127 the "From" field of the notification mail. If not set, take the user
128 from the pushing repo. Default: False.
128 from the pushing repo. Default: False.
129
129
130 If set, the following entries will also be used to customize the
130 If set, the following entries will also be used to customize the
131 notifications:
131 notifications:
132
132
133 email.from
133 email.from
134 Email ``From`` address to use if none can be found in the generated
134 Email ``From`` address to use if none can be found in the generated
135 email content.
135 email content.
136
136
137 web.baseurl
137 web.baseurl
138 Root repository URL to combine with repository paths when making
138 Root repository URL to combine with repository paths when making
139 references. See also ``notify.strip``.
139 references. See also ``notify.strip``.
140
140
141 '''
141 '''
142 from __future__ import absolute_import
142 from __future__ import absolute_import
143
143
144 import email
144 import email
145 import email.parser as emailparser
145 import email.parser as emailparser
146 import fnmatch
146 import fnmatch
147 import socket
147 import socket
148 import time
148 import time
149
149
150 from mercurial.i18n import _
150 from mercurial.i18n import _
151 from mercurial import (
151 from mercurial import (
152 error,
152 error,
153 logcmdutil,
153 logcmdutil,
154 mail,
154 mail,
155 patch,
155 patch,
156 pycompat,
156 registrar,
157 registrar,
157 util,
158 util,
158 )
159 )
159 from mercurial.utils import (
160 from mercurial.utils import (
160 dateutil,
161 dateutil,
161 stringutil,
162 stringutil,
162 )
163 )
163
164
165 if pycompat.ispy3:
166 import email.errors as emailerrors
167 else:
168 emailerrors = email.Errors
169
164 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
170 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
165 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
171 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
166 # be specifying the version(s) of Mercurial they are tested with, or
172 # be specifying the version(s) of Mercurial they are tested with, or
167 # leave the attribute unspecified.
173 # leave the attribute unspecified.
168 testedwith = 'ships-with-hg-core'
174 testedwith = 'ships-with-hg-core'
169
175
170 configtable = {}
176 configtable = {}
171 configitem = registrar.configitem(configtable)
177 configitem = registrar.configitem(configtable)
172
178
173 configitem('notify', 'changegroup',
179 configitem('notify', 'changegroup',
174 default=None,
180 default=None,
175 )
181 )
176 configitem('notify', 'config',
182 configitem('notify', 'config',
177 default=None,
183 default=None,
178 )
184 )
179 configitem('notify', 'diffstat',
185 configitem('notify', 'diffstat',
180 default=True,
186 default=True,
181 )
187 )
182 configitem('notify', 'domain',
188 configitem('notify', 'domain',
183 default=None,
189 default=None,
184 )
190 )
185 configitem('notify', 'fromauthor',
191 configitem('notify', 'fromauthor',
186 default=None,
192 default=None,
187 )
193 )
188 configitem('notify', 'incoming',
194 configitem('notify', 'incoming',
189 default=None,
195 default=None,
190 )
196 )
191 configitem('notify', 'maxdiff',
197 configitem('notify', 'maxdiff',
192 default=300,
198 default=300,
193 )
199 )
194 configitem('notify', 'maxdiffstat',
200 configitem('notify', 'maxdiffstat',
195 default=-1,
201 default=-1,
196 )
202 )
197 configitem('notify', 'maxsubject',
203 configitem('notify', 'maxsubject',
198 default=67,
204 default=67,
199 )
205 )
200 configitem('notify', 'mbox',
206 configitem('notify', 'mbox',
201 default=None,
207 default=None,
202 )
208 )
203 configitem('notify', 'merge',
209 configitem('notify', 'merge',
204 default=True,
210 default=True,
205 )
211 )
206 configitem('notify', 'outgoing',
212 configitem('notify', 'outgoing',
207 default=None,
213 default=None,
208 )
214 )
209 configitem('notify', 'sources',
215 configitem('notify', 'sources',
210 default='serve',
216 default='serve',
211 )
217 )
212 configitem('notify', 'showfunc',
218 configitem('notify', 'showfunc',
213 default=None,
219 default=None,
214 )
220 )
215 configitem('notify', 'strip',
221 configitem('notify', 'strip',
216 default=0,
222 default=0,
217 )
223 )
218 configitem('notify', 'style',
224 configitem('notify', 'style',
219 default=None,
225 default=None,
220 )
226 )
221 configitem('notify', 'template',
227 configitem('notify', 'template',
222 default=None,
228 default=None,
223 )
229 )
224 configitem('notify', 'test',
230 configitem('notify', 'test',
225 default=True,
231 default=True,
226 )
232 )
227
233
228 # template for single changeset can include email headers.
234 # template for single changeset can include email headers.
229 single_template = b'''
235 single_template = b'''
230 Subject: changeset in {webroot}: {desc|firstline|strip}
236 Subject: changeset in {webroot}: {desc|firstline|strip}
231 From: {author}
237 From: {author}
232
238
233 changeset {node|short} in {root}
239 changeset {node|short} in {root}
234 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
240 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
235 description:
241 description:
236 \t{desc|tabindent|strip}
242 \t{desc|tabindent|strip}
237 '''.lstrip()
243 '''.lstrip()
238
244
239 # template for multiple changesets should not contain email headers,
245 # template for multiple changesets should not contain email headers,
240 # because only first set of headers will be used and result will look
246 # because only first set of headers will be used and result will look
241 # strange.
247 # strange.
242 multiple_template = b'''
248 multiple_template = b'''
243 changeset {node|short} in {root}
249 changeset {node|short} in {root}
244 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
250 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
245 summary: {desc|firstline}
251 summary: {desc|firstline}
246 '''
252 '''
247
253
248 deftemplates = {
254 deftemplates = {
249 'changegroup': multiple_template,
255 'changegroup': multiple_template,
250 }
256 }
251
257
252 class notifier(object):
258 class notifier(object):
253 '''email notification class.'''
259 '''email notification class.'''
254
260
255 def __init__(self, ui, repo, hooktype):
261 def __init__(self, ui, repo, hooktype):
256 self.ui = ui
262 self.ui = ui
257 cfg = self.ui.config('notify', 'config')
263 cfg = self.ui.config('notify', 'config')
258 if cfg:
264 if cfg:
259 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
265 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
260 self.repo = repo
266 self.repo = repo
261 self.stripcount = int(self.ui.config('notify', 'strip'))
267 self.stripcount = int(self.ui.config('notify', 'strip'))
262 self.root = self.strip(self.repo.root)
268 self.root = self.strip(self.repo.root)
263 self.domain = self.ui.config('notify', 'domain')
269 self.domain = self.ui.config('notify', 'domain')
264 self.mbox = self.ui.config('notify', 'mbox')
270 self.mbox = self.ui.config('notify', 'mbox')
265 self.test = self.ui.configbool('notify', 'test')
271 self.test = self.ui.configbool('notify', 'test')
266 self.charsets = mail._charsets(self.ui)
272 self.charsets = mail._charsets(self.ui)
267 self.subs = self.subscribers()
273 self.subs = self.subscribers()
268 self.merge = self.ui.configbool('notify', 'merge')
274 self.merge = self.ui.configbool('notify', 'merge')
269 self.showfunc = self.ui.configbool('notify', 'showfunc')
275 self.showfunc = self.ui.configbool('notify', 'showfunc')
270 if self.showfunc is None:
276 if self.showfunc is None:
271 self.showfunc = self.ui.configbool('diff', 'showfunc')
277 self.showfunc = self.ui.configbool('diff', 'showfunc')
272
278
273 mapfile = None
279 mapfile = None
274 template = (self.ui.config('notify', hooktype) or
280 template = (self.ui.config('notify', hooktype) or
275 self.ui.config('notify', 'template'))
281 self.ui.config('notify', 'template'))
276 if not template:
282 if not template:
277 mapfile = self.ui.config('notify', 'style')
283 mapfile = self.ui.config('notify', 'style')
278 if not mapfile and not template:
284 if not mapfile and not template:
279 template = deftemplates.get(hooktype) or single_template
285 template = deftemplates.get(hooktype) or single_template
280 spec = logcmdutil.templatespec(template, mapfile)
286 spec = logcmdutil.templatespec(template, mapfile)
281 self.t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
287 self.t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
282
288
283 def strip(self, path):
289 def strip(self, path):
284 '''strip leading slashes from local path, turn into web-safe path.'''
290 '''strip leading slashes from local path, turn into web-safe path.'''
285
291
286 path = util.pconvert(path)
292 path = util.pconvert(path)
287 count = self.stripcount
293 count = self.stripcount
288 while count > 0:
294 while count > 0:
289 c = path.find('/')
295 c = path.find('/')
290 if c == -1:
296 if c == -1:
291 break
297 break
292 path = path[c + 1:]
298 path = path[c + 1:]
293 count -= 1
299 count -= 1
294 return path
300 return path
295
301
296 def fixmail(self, addr):
302 def fixmail(self, addr):
297 '''try to clean up email addresses.'''
303 '''try to clean up email addresses.'''
298
304
299 addr = stringutil.email(addr.strip())
305 addr = stringutil.email(addr.strip())
300 if self.domain:
306 if self.domain:
301 a = addr.find('@localhost')
307 a = addr.find('@localhost')
302 if a != -1:
308 if a != -1:
303 addr = addr[:a]
309 addr = addr[:a]
304 if '@' not in addr:
310 if '@' not in addr:
305 return addr + '@' + self.domain
311 return addr + '@' + self.domain
306 return addr
312 return addr
307
313
308 def subscribers(self):
314 def subscribers(self):
309 '''return list of email addresses of subscribers to this repo.'''
315 '''return list of email addresses of subscribers to this repo.'''
310 subs = set()
316 subs = set()
311 for user, pats in self.ui.configitems('usersubs'):
317 for user, pats in self.ui.configitems('usersubs'):
312 for pat in pats.split(','):
318 for pat in pats.split(','):
313 if '#' in pat:
319 if '#' in pat:
314 pat, revs = pat.split('#', 1)
320 pat, revs = pat.split('#', 1)
315 else:
321 else:
316 revs = None
322 revs = None
317 if fnmatch.fnmatch(self.repo.root, pat.strip()):
323 if fnmatch.fnmatch(self.repo.root, pat.strip()):
318 subs.add((self.fixmail(user), revs))
324 subs.add((self.fixmail(user), revs))
319 for pat, users in self.ui.configitems('reposubs'):
325 for pat, users in self.ui.configitems('reposubs'):
320 if '#' in pat:
326 if '#' in pat:
321 pat, revs = pat.split('#', 1)
327 pat, revs = pat.split('#', 1)
322 else:
328 else:
323 revs = None
329 revs = None
324 if fnmatch.fnmatch(self.repo.root, pat):
330 if fnmatch.fnmatch(self.repo.root, pat):
325 for user in users.split(','):
331 for user in users.split(','):
326 subs.add((self.fixmail(user), revs))
332 subs.add((self.fixmail(user), revs))
327 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
333 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
328 for s, r in sorted(subs)]
334 for s, r in sorted(subs)]
329
335
330 def node(self, ctx, **props):
336 def node(self, ctx, **props):
331 '''format one changeset, unless it is a suppressed merge.'''
337 '''format one changeset, unless it is a suppressed merge.'''
332 if not self.merge and len(ctx.parents()) > 1:
338 if not self.merge and len(ctx.parents()) > 1:
333 return False
339 return False
334 self.t.show(ctx, changes=ctx.changeset(),
340 self.t.show(ctx, changes=ctx.changeset(),
335 baseurl=self.ui.config('web', 'baseurl'),
341 baseurl=self.ui.config('web', 'baseurl'),
336 root=self.repo.root, webroot=self.root, **props)
342 root=self.repo.root, webroot=self.root, **props)
337 return True
343 return True
338
344
339 def skipsource(self, source):
345 def skipsource(self, source):
340 '''true if incoming changes from this source should be skipped.'''
346 '''true if incoming changes from this source should be skipped.'''
341 ok_sources = self.ui.config('notify', 'sources').split()
347 ok_sources = self.ui.config('notify', 'sources').split()
342 return source not in ok_sources
348 return source not in ok_sources
343
349
344 def send(self, ctx, count, data):
350 def send(self, ctx, count, data):
345 '''send message.'''
351 '''send message.'''
346
352
347 # Select subscribers by revset
353 # Select subscribers by revset
348 subs = set()
354 subs = set()
349 for sub, spec in self.subs:
355 for sub, spec in self.subs:
350 if spec is None:
356 if spec is None:
351 subs.add(sub)
357 subs.add(sub)
352 continue
358 continue
353 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
359 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
354 if len(revs):
360 if len(revs):
355 subs.add(sub)
361 subs.add(sub)
356 continue
362 continue
357 if len(subs) == 0:
363 if len(subs) == 0:
358 self.ui.debug('notify: no subscribers to selected repo '
364 self.ui.debug('notify: no subscribers to selected repo '
359 'and revset\n')
365 'and revset\n')
360 return
366 return
361
367
362 p = emailparser.Parser()
368 p = emailparser.Parser()
363 try:
369 try:
364 msg = p.parsestr(data)
370 msg = p.parsestr(data)
365 except email.Errors.MessageParseError as inst:
371 except emailerrors.MessageParseError as inst:
366 raise error.Abort(inst)
372 raise error.Abort(inst)
367
373
368 # store sender and subject
374 # store sender and subject
369 sender, subject = msg['From'], msg['Subject']
375 sender, subject = msg['From'], msg['Subject']
370 del msg['From'], msg['Subject']
376 del msg['From'], msg['Subject']
371
377
372 if not msg.is_multipart():
378 if not msg.is_multipart():
373 # create fresh mime message from scratch
379 # create fresh mime message from scratch
374 # (multipart templates must take care of this themselves)
380 # (multipart templates must take care of this themselves)
375 headers = msg.items()
381 headers = msg.items()
376 payload = msg.get_payload()
382 payload = msg.get_payload()
377 # for notification prefer readability over data precision
383 # for notification prefer readability over data precision
378 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
384 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
379 # reinstate custom headers
385 # reinstate custom headers
380 for k, v in headers:
386 for k, v in headers:
381 msg[k] = v
387 msg[k] = v
382
388
383 msg['Date'] = dateutil.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
389 msg['Date'] = dateutil.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
384
390
385 # try to make subject line exist and be useful
391 # try to make subject line exist and be useful
386 if not subject:
392 if not subject:
387 if count > 1:
393 if count > 1:
388 subject = _('%s: %d new changesets') % (self.root, count)
394 subject = _('%s: %d new changesets') % (self.root, count)
389 else:
395 else:
390 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
396 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
391 subject = '%s: %s' % (self.root, s)
397 subject = '%s: %s' % (self.root, s)
392 maxsubject = int(self.ui.config('notify', 'maxsubject'))
398 maxsubject = int(self.ui.config('notify', 'maxsubject'))
393 if maxsubject:
399 if maxsubject:
394 subject = stringutil.ellipsis(subject, maxsubject)
400 subject = stringutil.ellipsis(subject, maxsubject)
395 msg['Subject'] = mail.headencode(self.ui, subject,
401 msg['Subject'] = mail.headencode(self.ui, subject,
396 self.charsets, self.test)
402 self.charsets, self.test)
397
403
398 # try to make message have proper sender
404 # try to make message have proper sender
399 if not sender:
405 if not sender:
400 sender = self.ui.config('email', 'from') or self.ui.username()
406 sender = self.ui.config('email', 'from') or self.ui.username()
401 if '@' not in sender or '@localhost' in sender:
407 if '@' not in sender or '@localhost' in sender:
402 sender = self.fixmail(sender)
408 sender = self.fixmail(sender)
403 msg['From'] = mail.addressencode(self.ui, sender,
409 msg['From'] = mail.addressencode(self.ui, sender,
404 self.charsets, self.test)
410 self.charsets, self.test)
405
411
406 msg['X-Hg-Notification'] = 'changeset %s' % ctx
412 msg['X-Hg-Notification'] = 'changeset %s' % ctx
407 if not msg['Message-Id']:
413 if not msg['Message-Id']:
408 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
414 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
409 (ctx, int(time.time()),
415 (ctx, int(time.time()),
410 hash(self.repo.root), socket.getfqdn()))
416 hash(self.repo.root), socket.getfqdn()))
411 msg['To'] = ', '.join(sorted(subs))
417 msg['To'] = ', '.join(sorted(subs))
412
418
413 msgtext = msg.as_string()
419 msgtext = msg.as_string()
414 if self.test:
420 if self.test:
415 self.ui.write(msgtext)
421 self.ui.write(msgtext)
416 if not msgtext.endswith('\n'):
422 if not msgtext.endswith('\n'):
417 self.ui.write('\n')
423 self.ui.write('\n')
418 else:
424 else:
419 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
425 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
420 (len(subs), count))
426 (len(subs), count))
421 mail.sendmail(self.ui, stringutil.email(msg['From']),
427 mail.sendmail(self.ui, stringutil.email(msg['From']),
422 subs, msgtext, mbox=self.mbox)
428 subs, msgtext, mbox=self.mbox)
423
429
424 def diff(self, ctx, ref=None):
430 def diff(self, ctx, ref=None):
425
431
426 maxdiff = int(self.ui.config('notify', 'maxdiff'))
432 maxdiff = int(self.ui.config('notify', 'maxdiff'))
427 prev = ctx.p1().node()
433 prev = ctx.p1().node()
428 if ref:
434 if ref:
429 ref = ref.node()
435 ref = ref.node()
430 else:
436 else:
431 ref = ctx.node()
437 ref = ctx.node()
432 diffopts = patch.diffallopts(self.ui)
438 diffopts = patch.diffallopts(self.ui)
433 diffopts.showfunc = self.showfunc
439 diffopts.showfunc = self.showfunc
434 chunks = patch.diff(self.repo, prev, ref, opts=diffopts)
440 chunks = patch.diff(self.repo, prev, ref, opts=diffopts)
435 difflines = ''.join(chunks).splitlines()
441 difflines = ''.join(chunks).splitlines()
436
442
437 if self.ui.configbool('notify', 'diffstat'):
443 if self.ui.configbool('notify', 'diffstat'):
438 maxdiffstat = int(self.ui.config('notify', 'maxdiffstat'))
444 maxdiffstat = int(self.ui.config('notify', 'maxdiffstat'))
439 s = patch.diffstat(difflines)
445 s = patch.diffstat(difflines)
440 # s may be nil, don't include the header if it is
446 # s may be nil, don't include the header if it is
441 if s:
447 if s:
442 if maxdiffstat >= 0 and s.count("\n") > maxdiffstat + 1:
448 if maxdiffstat >= 0 and s.count("\n") > maxdiffstat + 1:
443 s = s.split("\n")
449 s = s.split("\n")
444 msg = _('\ndiffstat (truncated from %d to %d lines):\n\n')
450 msg = _('\ndiffstat (truncated from %d to %d lines):\n\n')
445 self.ui.write(msg % (len(s) - 2, maxdiffstat))
451 self.ui.write(msg % (len(s) - 2, maxdiffstat))
446 self.ui.write("\n".join(s[:maxdiffstat] + s[-2:]))
452 self.ui.write("\n".join(s[:maxdiffstat] + s[-2:]))
447 else:
453 else:
448 self.ui.write(_('\ndiffstat:\n\n%s') % s)
454 self.ui.write(_('\ndiffstat:\n\n%s') % s)
449
455
450 if maxdiff == 0:
456 if maxdiff == 0:
451 return
457 return
452 elif maxdiff > 0 and len(difflines) > maxdiff:
458 elif maxdiff > 0 and len(difflines) > maxdiff:
453 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
459 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
454 self.ui.write(msg % (len(difflines), maxdiff))
460 self.ui.write(msg % (len(difflines), maxdiff))
455 difflines = difflines[:maxdiff]
461 difflines = difflines[:maxdiff]
456 elif difflines:
462 elif difflines:
457 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
463 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
458
464
459 self.ui.write("\n".join(difflines))
465 self.ui.write("\n".join(difflines))
460
466
461 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
467 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
462 '''send email notifications to interested subscribers.
468 '''send email notifications to interested subscribers.
463
469
464 if used as changegroup hook, send one email for all changesets in
470 if used as changegroup hook, send one email for all changesets in
465 changegroup. else send one email per changeset.'''
471 changegroup. else send one email per changeset.'''
466
472
467 n = notifier(ui, repo, hooktype)
473 n = notifier(ui, repo, hooktype)
468 ctx = repo.unfiltered()[node]
474 ctx = repo.unfiltered()[node]
469
475
470 if not n.subs:
476 if not n.subs:
471 ui.debug('notify: no subscribers to repository %s\n' % n.root)
477 ui.debug('notify: no subscribers to repository %s\n' % n.root)
472 return
478 return
473 if n.skipsource(source):
479 if n.skipsource(source):
474 ui.debug('notify: changes have source "%s" - skipping\n' % source)
480 ui.debug('notify: changes have source "%s" - skipping\n' % source)
475 return
481 return
476
482
477 ui.pushbuffer()
483 ui.pushbuffer()
478 data = ''
484 data = ''
479 count = 0
485 count = 0
480 author = ''
486 author = ''
481 if hooktype == 'changegroup' or hooktype == 'outgoing':
487 if hooktype == 'changegroup' or hooktype == 'outgoing':
482 for rev in repo.changelog.revs(start=ctx.rev()):
488 for rev in repo.changelog.revs(start=ctx.rev()):
483 if n.node(repo[rev]):
489 if n.node(repo[rev]):
484 count += 1
490 count += 1
485 if not author:
491 if not author:
486 author = repo[rev].user()
492 author = repo[rev].user()
487 else:
493 else:
488 data += ui.popbuffer()
494 data += ui.popbuffer()
489 ui.note(_('notify: suppressing notification for merge %d:%s\n')
495 ui.note(_('notify: suppressing notification for merge %d:%s\n')
490 % (rev, repo[rev].hex()[:12]))
496 % (rev, repo[rev].hex()[:12]))
491 ui.pushbuffer()
497 ui.pushbuffer()
492 if count:
498 if count:
493 n.diff(ctx, repo['tip'])
499 n.diff(ctx, repo['tip'])
494 elif ctx.rev() in repo:
500 elif ctx.rev() in repo:
495 if not n.node(ctx):
501 if not n.node(ctx):
496 ui.popbuffer()
502 ui.popbuffer()
497 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
503 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
498 (ctx.rev(), ctx.hex()[:12]))
504 (ctx.rev(), ctx.hex()[:12]))
499 return
505 return
500 count += 1
506 count += 1
501 n.diff(ctx)
507 n.diff(ctx)
502 if not author:
508 if not author:
503 author = ctx.user()
509 author = ctx.user()
504
510
505 data += ui.popbuffer()
511 data += ui.popbuffer()
506 fromauthor = ui.config('notify', 'fromauthor')
512 fromauthor = ui.config('notify', 'fromauthor')
507 if author and fromauthor:
513 if author and fromauthor:
508 data = '\n'.join(['From: %s' % author, data])
514 data = '\n'.join(['From: %s' % author, data])
509
515
510 if count:
516 if count:
511 n.send(ctx, count, data)
517 n.send(ctx, count, data)
General Comments 0
You need to be logged in to leave comments. Login now