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