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