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