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