##// END OF EJS Templates
py3: use as_bytes() method of EmailMessage...
Denis Laxalde -
r43630:7d912413 stable
parent child Browse files
Show More
@@ -1,574 +1,574 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.parser as emailparser
151 import email.parser as emailparser
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 p = emailparser.Parser()
385 p = emailparser.Parser()
386 try:
386 try:
387 msg = p.parsestr(encoding.strfromlocal(data))
387 msg = p.parsestr(encoding.strfromlocal(data))
388 except emailerrors.MessageParseError as inst:
388 except emailerrors.MessageParseError as inst:
389 raise error.Abort(inst)
389 raise error.Abort(inst)
390
390
391 # store sender and subject
391 # store sender and subject
392 sender = msg[r'From']
392 sender = msg[r'From']
393 subject = msg[r'Subject']
393 subject = msg[r'Subject']
394 if sender is not None:
394 if sender is not None:
395 sender = encoding.strtolocal(sender)
395 sender = encoding.strtolocal(sender)
396 if subject is not None:
396 if subject is not None:
397 subject = encoding.strtolocal(subject)
397 subject = encoding.strtolocal(subject)
398 del msg[r'From'], msg[r'Subject']
398 del msg[r'From'], msg[r'Subject']
399
399
400 if not msg.is_multipart():
400 if not msg.is_multipart():
401 # create fresh mime message from scratch
401 # create fresh mime message from scratch
402 # (multipart templates must take care of this themselves)
402 # (multipart templates must take care of this themselves)
403 headers = msg.items()
403 headers = msg.items()
404 payload = msg.get_payload()
404 payload = msg.get_payload()
405 # for notification prefer readability over data precision
405 # for notification prefer readability over data precision
406 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
406 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
407 # reinstate custom headers
407 # reinstate custom headers
408 for k, v in headers:
408 for k, v in headers:
409 msg[k] = v
409 msg[k] = v
410
410
411 msg[r'Date'] = encoding.strfromlocal(
411 msg[r'Date'] = encoding.strfromlocal(
412 dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2")
412 dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2")
413 )
413 )
414
414
415 # try to make subject line exist and be useful
415 # try to make subject line exist and be useful
416 if not subject:
416 if not subject:
417 if count > 1:
417 if count > 1:
418 subject = _(b'%s: %d new changesets') % (self.root, count)
418 subject = _(b'%s: %d new changesets') % (self.root, count)
419 else:
419 else:
420 s = ctx.description().lstrip().split(b'\n', 1)[0].rstrip()
420 s = ctx.description().lstrip().split(b'\n', 1)[0].rstrip()
421 subject = b'%s: %s' % (self.root, s)
421 subject = b'%s: %s' % (self.root, s)
422 maxsubject = int(self.ui.config(b'notify', b'maxsubject'))
422 maxsubject = int(self.ui.config(b'notify', b'maxsubject'))
423 if maxsubject:
423 if maxsubject:
424 subject = stringutil.ellipsis(subject, maxsubject)
424 subject = stringutil.ellipsis(subject, maxsubject)
425 msg[r'Subject'] = encoding.strfromlocal(
425 msg[r'Subject'] = encoding.strfromlocal(
426 mail.headencode(self.ui, subject, self.charsets, self.test)
426 mail.headencode(self.ui, subject, self.charsets, self.test)
427 )
427 )
428
428
429 # try to make message have proper sender
429 # try to make message have proper sender
430 if not sender:
430 if not sender:
431 sender = self.ui.config(b'email', b'from') or self.ui.username()
431 sender = self.ui.config(b'email', b'from') or self.ui.username()
432 if b'@' not in sender or b'@localhost' in sender:
432 if b'@' not in sender or b'@localhost' in sender:
433 sender = self.fixmail(sender)
433 sender = self.fixmail(sender)
434 msg[r'From'] = encoding.strfromlocal(
434 msg[r'From'] = encoding.strfromlocal(
435 mail.addressencode(self.ui, sender, self.charsets, self.test)
435 mail.addressencode(self.ui, sender, self.charsets, self.test)
436 )
436 )
437
437
438 msg[r'X-Hg-Notification'] = r'changeset %s' % ctx
438 msg[r'X-Hg-Notification'] = r'changeset %s' % ctx
439 if not msg[r'Message-Id']:
439 if not msg[r'Message-Id']:
440 msg[r'Message-Id'] = messageid(ctx, self.domain, self.messageidseed)
440 msg[r'Message-Id'] = messageid(ctx, self.domain, self.messageidseed)
441 msg[r'To'] = encoding.strfromlocal(b', '.join(sorted(subs)))
441 msg[r'To'] = encoding.strfromlocal(b', '.join(sorted(subs)))
442
442
443 msgtext = encoding.strtolocal(msg.as_string())
443 msgtext = msg.as_bytes() if pycompat.ispy3 else msg.as_string()
444 if self.test:
444 if self.test:
445 self.ui.write(msgtext)
445 self.ui.write(msgtext)
446 if not msgtext.endswith(b'\n'):
446 if not msgtext.endswith(b'\n'):
447 self.ui.write(b'\n')
447 self.ui.write(b'\n')
448 else:
448 else:
449 self.ui.status(
449 self.ui.status(
450 _(b'notify: sending %d subscribers %d changes\n')
450 _(b'notify: sending %d subscribers %d changes\n')
451 % (len(subs), count)
451 % (len(subs), count)
452 )
452 )
453 mail.sendmail(
453 mail.sendmail(
454 self.ui,
454 self.ui,
455 stringutil.email(msg[r'From']),
455 stringutil.email(msg[r'From']),
456 subs,
456 subs,
457 msgtext,
457 msgtext,
458 mbox=self.mbox,
458 mbox=self.mbox,
459 )
459 )
460
460
461 def diff(self, ctx, ref=None):
461 def diff(self, ctx, ref=None):
462
462
463 maxdiff = int(self.ui.config(b'notify', b'maxdiff'))
463 maxdiff = int(self.ui.config(b'notify', b'maxdiff'))
464 prev = ctx.p1().node()
464 prev = ctx.p1().node()
465 if ref:
465 if ref:
466 ref = ref.node()
466 ref = ref.node()
467 else:
467 else:
468 ref = ctx.node()
468 ref = ctx.node()
469 diffopts = patch.diffallopts(self.ui)
469 diffopts = patch.diffallopts(self.ui)
470 diffopts.showfunc = self.showfunc
470 diffopts.showfunc = self.showfunc
471 chunks = patch.diff(self.repo, prev, ref, opts=diffopts)
471 chunks = patch.diff(self.repo, prev, ref, opts=diffopts)
472 difflines = b''.join(chunks).splitlines()
472 difflines = b''.join(chunks).splitlines()
473
473
474 if self.ui.configbool(b'notify', b'diffstat'):
474 if self.ui.configbool(b'notify', b'diffstat'):
475 maxdiffstat = int(self.ui.config(b'notify', b'maxdiffstat'))
475 maxdiffstat = int(self.ui.config(b'notify', b'maxdiffstat'))
476 s = patch.diffstat(difflines)
476 s = patch.diffstat(difflines)
477 # s may be nil, don't include the header if it is
477 # s may be nil, don't include the header if it is
478 if s:
478 if s:
479 if maxdiffstat >= 0 and s.count(b"\n") > maxdiffstat + 1:
479 if maxdiffstat >= 0 and s.count(b"\n") > maxdiffstat + 1:
480 s = s.split(b"\n")
480 s = s.split(b"\n")
481 msg = _(b'\ndiffstat (truncated from %d to %d lines):\n\n')
481 msg = _(b'\ndiffstat (truncated from %d to %d lines):\n\n')
482 self.ui.write(msg % (len(s) - 2, maxdiffstat))
482 self.ui.write(msg % (len(s) - 2, maxdiffstat))
483 self.ui.write(b"\n".join(s[:maxdiffstat] + s[-2:]))
483 self.ui.write(b"\n".join(s[:maxdiffstat] + s[-2:]))
484 else:
484 else:
485 self.ui.write(_(b'\ndiffstat:\n\n%s') % s)
485 self.ui.write(_(b'\ndiffstat:\n\n%s') % s)
486
486
487 if maxdiff == 0:
487 if maxdiff == 0:
488 return
488 return
489 elif maxdiff > 0 and len(difflines) > maxdiff:
489 elif maxdiff > 0 and len(difflines) > maxdiff:
490 msg = _(b'\ndiffs (truncated from %d to %d lines):\n\n')
490 msg = _(b'\ndiffs (truncated from %d to %d lines):\n\n')
491 self.ui.write(msg % (len(difflines), maxdiff))
491 self.ui.write(msg % (len(difflines), maxdiff))
492 difflines = difflines[:maxdiff]
492 difflines = difflines[:maxdiff]
493 elif difflines:
493 elif difflines:
494 self.ui.write(_(b'\ndiffs (%d lines):\n\n') % len(difflines))
494 self.ui.write(_(b'\ndiffs (%d lines):\n\n') % len(difflines))
495
495
496 self.ui.write(b"\n".join(difflines))
496 self.ui.write(b"\n".join(difflines))
497
497
498
498
499 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
499 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
500 '''send email notifications to interested subscribers.
500 '''send email notifications to interested subscribers.
501
501
502 if used as changegroup hook, send one email for all changesets in
502 if used as changegroup hook, send one email for all changesets in
503 changegroup. else send one email per changeset.'''
503 changegroup. else send one email per changeset.'''
504
504
505 n = notifier(ui, repo, hooktype)
505 n = notifier(ui, repo, hooktype)
506 ctx = repo.unfiltered()[node]
506 ctx = repo.unfiltered()[node]
507
507
508 if not n.subs:
508 if not n.subs:
509 ui.debug(b'notify: no subscribers to repository %s\n' % n.root)
509 ui.debug(b'notify: no subscribers to repository %s\n' % n.root)
510 return
510 return
511 if n.skipsource(source):
511 if n.skipsource(source):
512 ui.debug(b'notify: changes have source "%s" - skipping\n' % source)
512 ui.debug(b'notify: changes have source "%s" - skipping\n' % source)
513 return
513 return
514
514
515 ui.pushbuffer()
515 ui.pushbuffer()
516 data = b''
516 data = b''
517 count = 0
517 count = 0
518 author = b''
518 author = b''
519 if hooktype == b'changegroup' or hooktype == b'outgoing':
519 if hooktype == b'changegroup' or hooktype == b'outgoing':
520 for rev in repo.changelog.revs(start=ctx.rev()):
520 for rev in repo.changelog.revs(start=ctx.rev()):
521 if n.node(repo[rev]):
521 if n.node(repo[rev]):
522 count += 1
522 count += 1
523 if not author:
523 if not author:
524 author = repo[rev].user()
524 author = repo[rev].user()
525 else:
525 else:
526 data += ui.popbuffer()
526 data += ui.popbuffer()
527 ui.note(
527 ui.note(
528 _(b'notify: suppressing notification for merge %d:%s\n')
528 _(b'notify: suppressing notification for merge %d:%s\n')
529 % (rev, repo[rev].hex()[:12])
529 % (rev, repo[rev].hex()[:12])
530 )
530 )
531 ui.pushbuffer()
531 ui.pushbuffer()
532 if count:
532 if count:
533 n.diff(ctx, repo[b'tip'])
533 n.diff(ctx, repo[b'tip'])
534 elif ctx.rev() in repo:
534 elif ctx.rev() in repo:
535 if not n.node(ctx):
535 if not n.node(ctx):
536 ui.popbuffer()
536 ui.popbuffer()
537 ui.note(
537 ui.note(
538 _(b'notify: suppressing notification for merge %d:%s\n')
538 _(b'notify: suppressing notification for merge %d:%s\n')
539 % (ctx.rev(), ctx.hex()[:12])
539 % (ctx.rev(), ctx.hex()[:12])
540 )
540 )
541 return
541 return
542 count += 1
542 count += 1
543 n.diff(ctx)
543 n.diff(ctx)
544 if not author:
544 if not author:
545 author = ctx.user()
545 author = ctx.user()
546
546
547 data += ui.popbuffer()
547 data += ui.popbuffer()
548 fromauthor = ui.config(b'notify', b'fromauthor')
548 fromauthor = ui.config(b'notify', b'fromauthor')
549 if author and fromauthor:
549 if author and fromauthor:
550 data = b'\n'.join([b'From: %s' % author, data])
550 data = b'\n'.join([b'From: %s' % author, data])
551
551
552 if count:
552 if count:
553 n.send(ctx, count, data)
553 n.send(ctx, count, data)
554
554
555
555
556 def messageid(ctx, domain, messageidseed):
556 def messageid(ctx, domain, messageidseed):
557 if domain and messageidseed:
557 if domain and messageidseed:
558 host = domain
558 host = domain
559 else:
559 else:
560 host = encoding.strtolocal(socket.getfqdn())
560 host = encoding.strtolocal(socket.getfqdn())
561 if messageidseed:
561 if messageidseed:
562 messagehash = hashlib.sha512(ctx.hex() + messageidseed)
562 messagehash = hashlib.sha512(ctx.hex() + messageidseed)
563 messageid = b'<hg.%s@%s>' % (
563 messageid = b'<hg.%s@%s>' % (
564 pycompat.sysbytes(messagehash.hexdigest()[:64]),
564 pycompat.sysbytes(messagehash.hexdigest()[:64]),
565 host,
565 host,
566 )
566 )
567 else:
567 else:
568 messageid = b'<hg.%s.%d.%d@%s>' % (
568 messageid = b'<hg.%s.%d.%d@%s>' % (
569 ctx,
569 ctx,
570 int(time.time()),
570 int(time.time()),
571 hash(ctx.repo().root),
571 hash(ctx.repo().root),
572 host,
572 host,
573 )
573 )
574 return encoding.strfromlocal(messageid)
574 return encoding.strfromlocal(messageid)
General Comments 0
You need to be logged in to leave comments. Login now