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