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