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