##// END OF EJS Templates
notify: force an exception wrapped by errors.Abort to bytestr...
Matt Harbison -
r50759:5f006f78 default
parent child Browse files
Show More
@@ -1,658 +1,658
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 notify.reply-to-predecessor (EXPERIMENTAL)
136 notify.reply-to-predecessor (EXPERIMENTAL)
137 If set and the changeset has a predecessor in the repository, try to thread
137 If set and the changeset has a predecessor in the repository, try to thread
138 the notification mail with the predecessor. This adds the "In-Reply-To" header
138 the notification mail with the predecessor. This adds the "In-Reply-To" header
139 to the notification mail with a reference to the predecessor with the smallest
139 to the notification mail with a reference to the predecessor with the smallest
140 revision number. Mail threads can still be torn, especially when changesets
140 revision number. Mail threads can still be torn, especially when changesets
141 are folded.
141 are folded.
142
142
143 This option must be used in combination with ``notify.messageidseed``.
143 This option must be used in combination with ``notify.messageidseed``.
144
144
145 If set, the following entries will also be used to customize the
145 If set, the following entries will also be used to customize the
146 notifications:
146 notifications:
147
147
148 email.from
148 email.from
149 Email ``From`` address to use if none can be found in the generated
149 Email ``From`` address to use if none can be found in the generated
150 email content.
150 email content.
151
151
152 web.baseurl
152 web.baseurl
153 Root repository URL to combine with repository paths when making
153 Root repository URL to combine with repository paths when making
154 references. See also ``notify.strip``.
154 references. See also ``notify.strip``.
155
155
156 '''
156 '''
157
157
158 import email.errors as emailerrors
158 import email.errors as emailerrors
159 import email.utils as emailutils
159 import email.utils as emailutils
160 import fnmatch
160 import fnmatch
161 import hashlib
161 import hashlib
162 import socket
162 import socket
163 import time
163 import time
164
164
165 from mercurial.i18n import _
165 from mercurial.i18n import _
166 from mercurial import (
166 from mercurial import (
167 encoding,
167 encoding,
168 error,
168 error,
169 logcmdutil,
169 logcmdutil,
170 mail,
170 mail,
171 obsutil,
171 obsutil,
172 patch,
172 patch,
173 pycompat,
173 pycompat,
174 registrar,
174 registrar,
175 util,
175 util,
176 )
176 )
177 from mercurial.utils import (
177 from mercurial.utils import (
178 dateutil,
178 dateutil,
179 stringutil,
179 stringutil,
180 )
180 )
181
181
182 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
182 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
183 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
183 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
184 # be specifying the version(s) of Mercurial they are tested with, or
184 # be specifying the version(s) of Mercurial they are tested with, or
185 # leave the attribute unspecified.
185 # leave the attribute unspecified.
186 testedwith = b'ships-with-hg-core'
186 testedwith = b'ships-with-hg-core'
187
187
188 configtable = {}
188 configtable = {}
189 configitem = registrar.configitem(configtable)
189 configitem = registrar.configitem(configtable)
190
190
191 configitem(
191 configitem(
192 b'notify',
192 b'notify',
193 b'changegroup',
193 b'changegroup',
194 default=None,
194 default=None,
195 )
195 )
196 configitem(
196 configitem(
197 b'notify',
197 b'notify',
198 b'config',
198 b'config',
199 default=None,
199 default=None,
200 )
200 )
201 configitem(
201 configitem(
202 b'notify',
202 b'notify',
203 b'diffstat',
203 b'diffstat',
204 default=True,
204 default=True,
205 )
205 )
206 configitem(
206 configitem(
207 b'notify',
207 b'notify',
208 b'domain',
208 b'domain',
209 default=None,
209 default=None,
210 )
210 )
211 configitem(
211 configitem(
212 b'notify',
212 b'notify',
213 b'messageidseed',
213 b'messageidseed',
214 default=None,
214 default=None,
215 )
215 )
216 configitem(
216 configitem(
217 b'notify',
217 b'notify',
218 b'fromauthor',
218 b'fromauthor',
219 default=None,
219 default=None,
220 )
220 )
221 configitem(
221 configitem(
222 b'notify',
222 b'notify',
223 b'incoming',
223 b'incoming',
224 default=None,
224 default=None,
225 )
225 )
226 configitem(
226 configitem(
227 b'notify',
227 b'notify',
228 b'maxdiff',
228 b'maxdiff',
229 default=300,
229 default=300,
230 )
230 )
231 configitem(
231 configitem(
232 b'notify',
232 b'notify',
233 b'maxdiffstat',
233 b'maxdiffstat',
234 default=-1,
234 default=-1,
235 )
235 )
236 configitem(
236 configitem(
237 b'notify',
237 b'notify',
238 b'maxsubject',
238 b'maxsubject',
239 default=67,
239 default=67,
240 )
240 )
241 configitem(
241 configitem(
242 b'notify',
242 b'notify',
243 b'mbox',
243 b'mbox',
244 default=None,
244 default=None,
245 )
245 )
246 configitem(
246 configitem(
247 b'notify',
247 b'notify',
248 b'merge',
248 b'merge',
249 default=True,
249 default=True,
250 )
250 )
251 configitem(
251 configitem(
252 b'notify',
252 b'notify',
253 b'outgoing',
253 b'outgoing',
254 default=None,
254 default=None,
255 )
255 )
256 configitem(
256 configitem(
257 b'notify',
257 b'notify',
258 b'reply-to-predecessor',
258 b'reply-to-predecessor',
259 default=False,
259 default=False,
260 )
260 )
261 configitem(
261 configitem(
262 b'notify',
262 b'notify',
263 b'sources',
263 b'sources',
264 default=b'serve',
264 default=b'serve',
265 )
265 )
266 configitem(
266 configitem(
267 b'notify',
267 b'notify',
268 b'showfunc',
268 b'showfunc',
269 default=None,
269 default=None,
270 )
270 )
271 configitem(
271 configitem(
272 b'notify',
272 b'notify',
273 b'strip',
273 b'strip',
274 default=0,
274 default=0,
275 )
275 )
276 configitem(
276 configitem(
277 b'notify',
277 b'notify',
278 b'style',
278 b'style',
279 default=None,
279 default=None,
280 )
280 )
281 configitem(
281 configitem(
282 b'notify',
282 b'notify',
283 b'template',
283 b'template',
284 default=None,
284 default=None,
285 )
285 )
286 configitem(
286 configitem(
287 b'notify',
287 b'notify',
288 b'test',
288 b'test',
289 default=True,
289 default=True,
290 )
290 )
291
291
292 # template for single changeset can include email headers.
292 # template for single changeset can include email headers.
293 single_template = b'''
293 single_template = b'''
294 Subject: changeset in {webroot}: {desc|firstline|strip}
294 Subject: changeset in {webroot}: {desc|firstline|strip}
295 From: {author}
295 From: {author}
296
296
297 changeset {node|short} in {root}
297 changeset {node|short} in {root}
298 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
298 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
299 description:
299 description:
300 \t{desc|tabindent|strip}
300 \t{desc|tabindent|strip}
301 '''.lstrip()
301 '''.lstrip()
302
302
303 # template for multiple changesets should not contain email headers,
303 # template for multiple changesets should not contain email headers,
304 # because only first set of headers will be used and result will look
304 # because only first set of headers will be used and result will look
305 # strange.
305 # strange.
306 multiple_template = b'''
306 multiple_template = b'''
307 changeset {node|short} in {root}
307 changeset {node|short} in {root}
308 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
308 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
309 summary: {desc|firstline}
309 summary: {desc|firstline}
310 '''
310 '''
311
311
312 deftemplates = {
312 deftemplates = {
313 b'changegroup': multiple_template,
313 b'changegroup': multiple_template,
314 }
314 }
315
315
316
316
317 class notifier:
317 class notifier:
318 '''email notification class.'''
318 '''email notification class.'''
319
319
320 def __init__(self, ui, repo, hooktype):
320 def __init__(self, ui, repo, hooktype):
321 self.ui = ui
321 self.ui = ui
322 cfg = self.ui.config(b'notify', b'config')
322 cfg = self.ui.config(b'notify', b'config')
323 if cfg:
323 if cfg:
324 self.ui.readconfig(cfg, sections=[b'usersubs', b'reposubs'])
324 self.ui.readconfig(cfg, sections=[b'usersubs', b'reposubs'])
325 self.repo = repo
325 self.repo = repo
326 self.stripcount = int(self.ui.config(b'notify', b'strip'))
326 self.stripcount = int(self.ui.config(b'notify', b'strip'))
327 self.root = self.strip(self.repo.root)
327 self.root = self.strip(self.repo.root)
328 self.domain = self.ui.config(b'notify', b'domain')
328 self.domain = self.ui.config(b'notify', b'domain')
329 self.mbox = self.ui.config(b'notify', b'mbox')
329 self.mbox = self.ui.config(b'notify', b'mbox')
330 self.test = self.ui.configbool(b'notify', b'test')
330 self.test = self.ui.configbool(b'notify', b'test')
331 self.charsets = mail._charsets(self.ui)
331 self.charsets = mail._charsets(self.ui)
332 self.subs = self.subscribers()
332 self.subs = self.subscribers()
333 self.merge = self.ui.configbool(b'notify', b'merge')
333 self.merge = self.ui.configbool(b'notify', b'merge')
334 self.showfunc = self.ui.configbool(b'notify', b'showfunc')
334 self.showfunc = self.ui.configbool(b'notify', b'showfunc')
335 self.messageidseed = self.ui.config(b'notify', b'messageidseed')
335 self.messageidseed = self.ui.config(b'notify', b'messageidseed')
336 self.reply = self.ui.configbool(b'notify', b'reply-to-predecessor')
336 self.reply = self.ui.configbool(b'notify', b'reply-to-predecessor')
337
337
338 if self.reply and not self.messageidseed:
338 if self.reply and not self.messageidseed:
339 raise error.Abort(
339 raise error.Abort(
340 _(
340 _(
341 b'notify.reply-to-predecessor used without '
341 b'notify.reply-to-predecessor used without '
342 b'notify.messageidseed'
342 b'notify.messageidseed'
343 )
343 )
344 )
344 )
345
345
346 if self.showfunc is None:
346 if self.showfunc is None:
347 self.showfunc = self.ui.configbool(b'diff', b'showfunc')
347 self.showfunc = self.ui.configbool(b'diff', b'showfunc')
348
348
349 mapfile = None
349 mapfile = None
350 template = self.ui.config(b'notify', hooktype) or self.ui.config(
350 template = self.ui.config(b'notify', hooktype) or self.ui.config(
351 b'notify', b'template'
351 b'notify', b'template'
352 )
352 )
353 if not template:
353 if not template:
354 mapfile = self.ui.config(b'notify', b'style')
354 mapfile = self.ui.config(b'notify', b'style')
355 if not mapfile and not template:
355 if not mapfile and not template:
356 template = deftemplates.get(hooktype) or single_template
356 template = deftemplates.get(hooktype) or single_template
357 spec = logcmdutil.templatespec(template, mapfile)
357 spec = logcmdutil.templatespec(template, mapfile)
358 self.t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
358 self.t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
359
359
360 def strip(self, path):
360 def strip(self, path):
361 '''strip leading slashes from local path, turn into web-safe path.'''
361 '''strip leading slashes from local path, turn into web-safe path.'''
362
362
363 path = util.pconvert(path)
363 path = util.pconvert(path)
364 count = self.stripcount
364 count = self.stripcount
365 while count > 0:
365 while count > 0:
366 c = path.find(b'/')
366 c = path.find(b'/')
367 if c == -1:
367 if c == -1:
368 break
368 break
369 path = path[c + 1 :]
369 path = path[c + 1 :]
370 count -= 1
370 count -= 1
371 return path
371 return path
372
372
373 def fixmail(self, addr):
373 def fixmail(self, addr):
374 '''try to clean up email addresses.'''
374 '''try to clean up email addresses.'''
375
375
376 addr = stringutil.email(addr.strip())
376 addr = stringutil.email(addr.strip())
377 if self.domain:
377 if self.domain:
378 a = addr.find(b'@localhost')
378 a = addr.find(b'@localhost')
379 if a != -1:
379 if a != -1:
380 addr = addr[:a]
380 addr = addr[:a]
381 if b'@' not in addr:
381 if b'@' not in addr:
382 return addr + b'@' + self.domain
382 return addr + b'@' + self.domain
383 return addr
383 return addr
384
384
385 def subscribers(self):
385 def subscribers(self):
386 '''return list of email addresses of subscribers to this repo.'''
386 '''return list of email addresses of subscribers to this repo.'''
387 subs = set()
387 subs = set()
388 for user, pats in self.ui.configitems(b'usersubs'):
388 for user, pats in self.ui.configitems(b'usersubs'):
389 for pat in pats.split(b','):
389 for pat in pats.split(b','):
390 if b'#' in pat:
390 if b'#' in pat:
391 pat, revs = pat.split(b'#', 1)
391 pat, revs = pat.split(b'#', 1)
392 else:
392 else:
393 revs = None
393 revs = None
394 if fnmatch.fnmatch(self.repo.root, pat.strip()):
394 if fnmatch.fnmatch(self.repo.root, pat.strip()):
395 subs.add((self.fixmail(user), revs))
395 subs.add((self.fixmail(user), revs))
396 for pat, users in self.ui.configitems(b'reposubs'):
396 for pat, users in self.ui.configitems(b'reposubs'):
397 if b'#' in pat:
397 if b'#' in pat:
398 pat, revs = pat.split(b'#', 1)
398 pat, revs = pat.split(b'#', 1)
399 else:
399 else:
400 revs = None
400 revs = None
401 if fnmatch.fnmatch(self.repo.root, pat):
401 if fnmatch.fnmatch(self.repo.root, pat):
402 for user in users.split(b','):
402 for user in users.split(b','):
403 subs.add((self.fixmail(user), revs))
403 subs.add((self.fixmail(user), revs))
404 return [
404 return [
405 (mail.addressencode(self.ui, s, self.charsets, self.test), r)
405 (mail.addressencode(self.ui, s, self.charsets, self.test), r)
406 for s, r in sorted(subs)
406 for s, r in sorted(subs)
407 ]
407 ]
408
408
409 def node(self, ctx, **props):
409 def node(self, ctx, **props):
410 '''format one changeset, unless it is a suppressed merge.'''
410 '''format one changeset, unless it is a suppressed merge.'''
411 if not self.merge and len(ctx.parents()) > 1:
411 if not self.merge and len(ctx.parents()) > 1:
412 return False
412 return False
413 self.t.show(
413 self.t.show(
414 ctx,
414 ctx,
415 changes=ctx.changeset(),
415 changes=ctx.changeset(),
416 baseurl=self.ui.config(b'web', b'baseurl'),
416 baseurl=self.ui.config(b'web', b'baseurl'),
417 root=self.repo.root,
417 root=self.repo.root,
418 webroot=self.root,
418 webroot=self.root,
419 **props
419 **props
420 )
420 )
421 return True
421 return True
422
422
423 def skipsource(self, source):
423 def skipsource(self, source):
424 '''true if incoming changes from this source should be skipped.'''
424 '''true if incoming changes from this source should be skipped.'''
425 ok_sources = self.ui.config(b'notify', b'sources').split()
425 ok_sources = self.ui.config(b'notify', b'sources').split()
426 return source not in ok_sources
426 return source not in ok_sources
427
427
428 def send(self, ctx, count, data):
428 def send(self, ctx, count, data):
429 '''send message.'''
429 '''send message.'''
430
430
431 # Select subscribers by revset
431 # Select subscribers by revset
432 subs = set()
432 subs = set()
433 for sub, spec in self.subs:
433 for sub, spec in self.subs:
434 if spec is None:
434 if spec is None:
435 subs.add(sub)
435 subs.add(sub)
436 continue
436 continue
437 try:
437 try:
438 revs = self.repo.revs(b'%r and %d:', spec, ctx.rev())
438 revs = self.repo.revs(b'%r and %d:', spec, ctx.rev())
439 except error.RepoLookupError:
439 except error.RepoLookupError:
440 continue
440 continue
441 if len(revs):
441 if len(revs):
442 subs.add(sub)
442 subs.add(sub)
443 continue
443 continue
444 if len(subs) == 0:
444 if len(subs) == 0:
445 self.ui.debug(
445 self.ui.debug(
446 b'notify: no subscribers to selected repo and revset\n'
446 b'notify: no subscribers to selected repo and revset\n'
447 )
447 )
448 return
448 return
449
449
450 try:
450 try:
451 msg = mail.parsebytes(data)
451 msg = mail.parsebytes(data)
452 except emailerrors.MessageParseError as inst:
452 except emailerrors.MessageParseError as inst:
453 raise error.Abort(inst)
453 raise error.Abort(stringutil.forcebytestr(inst))
454
454
455 # store sender and subject
455 # store sender and subject
456 sender = msg['From']
456 sender = msg['From']
457 subject = msg['Subject']
457 subject = msg['Subject']
458 if sender is not None:
458 if sender is not None:
459 sender = mail.headdecode(sender)
459 sender = mail.headdecode(sender)
460 if subject is not None:
460 if subject is not None:
461 subject = mail.headdecode(subject)
461 subject = mail.headdecode(subject)
462 del msg['From'], msg['Subject']
462 del msg['From'], msg['Subject']
463
463
464 if not msg.is_multipart():
464 if not msg.is_multipart():
465 # create fresh mime message from scratch
465 # create fresh mime message from scratch
466 # (multipart templates must take care of this themselves)
466 # (multipart templates must take care of this themselves)
467 headers = msg.items()
467 headers = msg.items()
468 payload = msg.get_payload(decode=True)
468 payload = msg.get_payload(decode=True)
469 # for notification prefer readability over data precision
469 # for notification prefer readability over data precision
470 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
470 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
471 # reinstate custom headers
471 # reinstate custom headers
472 for k, v in headers:
472 for k, v in headers:
473 msg[k] = v
473 msg[k] = v
474
474
475 msg['Date'] = encoding.strfromlocal(
475 msg['Date'] = encoding.strfromlocal(
476 dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2")
476 dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2")
477 )
477 )
478
478
479 # try to make subject line exist and be useful
479 # try to make subject line exist and be useful
480 if not subject:
480 if not subject:
481 if count > 1:
481 if count > 1:
482 subject = _(b'%s: %d new changesets') % (self.root, count)
482 subject = _(b'%s: %d new changesets') % (self.root, count)
483 else:
483 else:
484 s = ctx.description().lstrip().split(b'\n', 1)[0].rstrip()
484 s = ctx.description().lstrip().split(b'\n', 1)[0].rstrip()
485 subject = b'%s: %s' % (self.root, s)
485 subject = b'%s: %s' % (self.root, s)
486 maxsubject = int(self.ui.config(b'notify', b'maxsubject'))
486 maxsubject = int(self.ui.config(b'notify', b'maxsubject'))
487 if maxsubject:
487 if maxsubject:
488 subject = stringutil.ellipsis(subject, maxsubject)
488 subject = stringutil.ellipsis(subject, maxsubject)
489 msg['Subject'] = mail.headencode(
489 msg['Subject'] = mail.headencode(
490 self.ui, subject, self.charsets, self.test
490 self.ui, subject, self.charsets, self.test
491 )
491 )
492
492
493 # try to make message have proper sender
493 # try to make message have proper sender
494 if not sender:
494 if not sender:
495 sender = self.ui.config(b'email', b'from') or self.ui.username()
495 sender = self.ui.config(b'email', b'from') or self.ui.username()
496 if b'@' not in sender or b'@localhost' in sender:
496 if b'@' not in sender or b'@localhost' in sender:
497 sender = self.fixmail(sender)
497 sender = self.fixmail(sender)
498 msg['From'] = mail.addressencode(
498 msg['From'] = mail.addressencode(
499 self.ui, sender, self.charsets, self.test
499 self.ui, sender, self.charsets, self.test
500 )
500 )
501
501
502 msg['X-Hg-Notification'] = 'changeset %s' % ctx
502 msg['X-Hg-Notification'] = 'changeset %s' % ctx
503 if not msg['Message-Id']:
503 if not msg['Message-Id']:
504 msg['Message-Id'] = messageid(ctx, self.domain, self.messageidseed)
504 msg['Message-Id'] = messageid(ctx, self.domain, self.messageidseed)
505 if self.reply:
505 if self.reply:
506 unfi = self.repo.unfiltered()
506 unfi = self.repo.unfiltered()
507 has_node = unfi.changelog.index.has_node
507 has_node = unfi.changelog.index.has_node
508 predecessors = [
508 predecessors = [
509 unfi[ctx2]
509 unfi[ctx2]
510 for ctx2 in obsutil.allpredecessors(unfi.obsstore, [ctx.node()])
510 for ctx2 in obsutil.allpredecessors(unfi.obsstore, [ctx.node()])
511 if ctx2 != ctx.node() and has_node(ctx2)
511 if ctx2 != ctx.node() and has_node(ctx2)
512 ]
512 ]
513 if predecessors:
513 if predecessors:
514 # There is at least one predecessor, so which to pick?
514 # There is at least one predecessor, so which to pick?
515 # Ideally, there is a unique root because changesets have
515 # Ideally, there is a unique root because changesets have
516 # been evolved/rebased one step at a time. In this case,
516 # been evolved/rebased one step at a time. In this case,
517 # just picking the oldest known changeset provides a stable
517 # just picking the oldest known changeset provides a stable
518 # base. It doesn't help when changesets are folded. Any
518 # base. It doesn't help when changesets are folded. Any
519 # better solution would require storing more information
519 # better solution would require storing more information
520 # in the repository.
520 # in the repository.
521 pred = min(predecessors, key=lambda ctx: ctx.rev())
521 pred = min(predecessors, key=lambda ctx: ctx.rev())
522 msg['In-Reply-To'] = messageid(
522 msg['In-Reply-To'] = messageid(
523 pred, self.domain, self.messageidseed
523 pred, self.domain, self.messageidseed
524 )
524 )
525 msg['To'] = ', '.join(sorted(subs))
525 msg['To'] = ', '.join(sorted(subs))
526
526
527 msgtext = msg.as_bytes()
527 msgtext = msg.as_bytes()
528 if self.test:
528 if self.test:
529 self.ui.write(msgtext)
529 self.ui.write(msgtext)
530 if not msgtext.endswith(b'\n'):
530 if not msgtext.endswith(b'\n'):
531 self.ui.write(b'\n')
531 self.ui.write(b'\n')
532 else:
532 else:
533 self.ui.status(
533 self.ui.status(
534 _(b'notify: sending %d subscribers %d changes\n')
534 _(b'notify: sending %d subscribers %d changes\n')
535 % (len(subs), count)
535 % (len(subs), count)
536 )
536 )
537 mail.sendmail(
537 mail.sendmail(
538 self.ui,
538 self.ui,
539 emailutils.parseaddr(msg['From'])[1],
539 emailutils.parseaddr(msg['From'])[1],
540 subs,
540 subs,
541 msgtext,
541 msgtext,
542 mbox=self.mbox,
542 mbox=self.mbox,
543 )
543 )
544
544
545 def diff(self, ctx, ref=None):
545 def diff(self, ctx, ref=None):
546
546
547 maxdiff = int(self.ui.config(b'notify', b'maxdiff'))
547 maxdiff = int(self.ui.config(b'notify', b'maxdiff'))
548 prev = ctx.p1().node()
548 prev = ctx.p1().node()
549 if ref:
549 if ref:
550 ref = ref.node()
550 ref = ref.node()
551 else:
551 else:
552 ref = ctx.node()
552 ref = ctx.node()
553 diffopts = patch.diffallopts(self.ui)
553 diffopts = patch.diffallopts(self.ui)
554 diffopts.showfunc = self.showfunc
554 diffopts.showfunc = self.showfunc
555 chunks = patch.diff(self.repo, prev, ref, opts=diffopts)
555 chunks = patch.diff(self.repo, prev, ref, opts=diffopts)
556 difflines = b''.join(chunks).splitlines()
556 difflines = b''.join(chunks).splitlines()
557
557
558 if self.ui.configbool(b'notify', b'diffstat'):
558 if self.ui.configbool(b'notify', b'diffstat'):
559 maxdiffstat = int(self.ui.config(b'notify', b'maxdiffstat'))
559 maxdiffstat = int(self.ui.config(b'notify', b'maxdiffstat'))
560 s = patch.diffstat(difflines)
560 s = patch.diffstat(difflines)
561 # s may be nil, don't include the header if it is
561 # s may be nil, don't include the header if it is
562 if s:
562 if s:
563 if maxdiffstat >= 0 and s.count(b"\n") > maxdiffstat + 1:
563 if maxdiffstat >= 0 and s.count(b"\n") > maxdiffstat + 1:
564 s = s.split(b"\n")
564 s = s.split(b"\n")
565 msg = _(b'\ndiffstat (truncated from %d to %d lines):\n\n')
565 msg = _(b'\ndiffstat (truncated from %d to %d lines):\n\n')
566 self.ui.write(msg % (len(s) - 2, maxdiffstat))
566 self.ui.write(msg % (len(s) - 2, maxdiffstat))
567 self.ui.write(b"\n".join(s[:maxdiffstat] + s[-2:]))
567 self.ui.write(b"\n".join(s[:maxdiffstat] + s[-2:]))
568 else:
568 else:
569 self.ui.write(_(b'\ndiffstat:\n\n%s') % s)
569 self.ui.write(_(b'\ndiffstat:\n\n%s') % s)
570
570
571 if maxdiff == 0:
571 if maxdiff == 0:
572 return
572 return
573 elif maxdiff > 0 and len(difflines) > maxdiff:
573 elif maxdiff > 0 and len(difflines) > maxdiff:
574 msg = _(b'\ndiffs (truncated from %d to %d lines):\n\n')
574 msg = _(b'\ndiffs (truncated from %d to %d lines):\n\n')
575 self.ui.write(msg % (len(difflines), maxdiff))
575 self.ui.write(msg % (len(difflines), maxdiff))
576 difflines = difflines[:maxdiff]
576 difflines = difflines[:maxdiff]
577 elif difflines:
577 elif difflines:
578 self.ui.write(_(b'\ndiffs (%d lines):\n\n') % len(difflines))
578 self.ui.write(_(b'\ndiffs (%d lines):\n\n') % len(difflines))
579
579
580 self.ui.write(b"\n".join(difflines))
580 self.ui.write(b"\n".join(difflines))
581
581
582
582
583 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
583 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
584 """send email notifications to interested subscribers.
584 """send email notifications to interested subscribers.
585
585
586 if used as changegroup hook, send one email for all changesets in
586 if used as changegroup hook, send one email for all changesets in
587 changegroup. else send one email per changeset."""
587 changegroup. else send one email per changeset."""
588
588
589 n = notifier(ui, repo, hooktype)
589 n = notifier(ui, repo, hooktype)
590 ctx = repo.unfiltered()[node]
590 ctx = repo.unfiltered()[node]
591
591
592 if not n.subs:
592 if not n.subs:
593 ui.debug(b'notify: no subscribers to repository %s\n' % n.root)
593 ui.debug(b'notify: no subscribers to repository %s\n' % n.root)
594 return
594 return
595 if n.skipsource(source):
595 if n.skipsource(source):
596 ui.debug(b'notify: changes have source "%s" - skipping\n' % source)
596 ui.debug(b'notify: changes have source "%s" - skipping\n' % source)
597 return
597 return
598
598
599 ui.pushbuffer()
599 ui.pushbuffer()
600 data = b''
600 data = b''
601 count = 0
601 count = 0
602 author = b''
602 author = b''
603 if hooktype == b'changegroup' or hooktype == b'outgoing':
603 if hooktype == b'changegroup' or hooktype == b'outgoing':
604 for rev in repo.changelog.revs(start=ctx.rev()):
604 for rev in repo.changelog.revs(start=ctx.rev()):
605 if n.node(repo[rev]):
605 if n.node(repo[rev]):
606 count += 1
606 count += 1
607 if not author:
607 if not author:
608 author = repo[rev].user()
608 author = repo[rev].user()
609 else:
609 else:
610 data += ui.popbuffer()
610 data += ui.popbuffer()
611 ui.note(
611 ui.note(
612 _(b'notify: suppressing notification for merge %d:%s\n')
612 _(b'notify: suppressing notification for merge %d:%s\n')
613 % (rev, repo[rev].hex()[:12])
613 % (rev, repo[rev].hex()[:12])
614 )
614 )
615 ui.pushbuffer()
615 ui.pushbuffer()
616 if count:
616 if count:
617 n.diff(ctx, repo[b'tip'])
617 n.diff(ctx, repo[b'tip'])
618 elif ctx.rev() in repo:
618 elif ctx.rev() in repo:
619 if not n.node(ctx):
619 if not n.node(ctx):
620 ui.popbuffer()
620 ui.popbuffer()
621 ui.note(
621 ui.note(
622 _(b'notify: suppressing notification for merge %d:%s\n')
622 _(b'notify: suppressing notification for merge %d:%s\n')
623 % (ctx.rev(), ctx.hex()[:12])
623 % (ctx.rev(), ctx.hex()[:12])
624 )
624 )
625 return
625 return
626 count += 1
626 count += 1
627 n.diff(ctx)
627 n.diff(ctx)
628 if not author:
628 if not author:
629 author = ctx.user()
629 author = ctx.user()
630
630
631 data += ui.popbuffer()
631 data += ui.popbuffer()
632 fromauthor = ui.config(b'notify', b'fromauthor')
632 fromauthor = ui.config(b'notify', b'fromauthor')
633 if author and fromauthor:
633 if author and fromauthor:
634 data = b'\n'.join([b'From: %s' % author, data])
634 data = b'\n'.join([b'From: %s' % author, data])
635
635
636 if count:
636 if count:
637 n.send(ctx, count, data)
637 n.send(ctx, count, data)
638
638
639
639
640 def messageid(ctx, domain, messageidseed):
640 def messageid(ctx, domain, messageidseed):
641 if domain and messageidseed:
641 if domain and messageidseed:
642 host = domain
642 host = domain
643 else:
643 else:
644 host = encoding.strtolocal(socket.getfqdn())
644 host = encoding.strtolocal(socket.getfqdn())
645 if messageidseed:
645 if messageidseed:
646 messagehash = hashlib.sha512(ctx.hex() + messageidseed)
646 messagehash = hashlib.sha512(ctx.hex() + messageidseed)
647 messageid = b'<hg.%s@%s>' % (
647 messageid = b'<hg.%s@%s>' % (
648 pycompat.sysbytes(messagehash.hexdigest()[:64]),
648 pycompat.sysbytes(messagehash.hexdigest()[:64]),
649 host,
649 host,
650 )
650 )
651 else:
651 else:
652 messageid = b'<hg.%s.%d.%d@%s>' % (
652 messageid = b'<hg.%s.%d.%d@%s>' % (
653 ctx,
653 ctx,
654 int(time.time()),
654 int(time.time()),
655 hash(ctx.repo().root),
655 hash(ctx.repo().root),
656 host,
656 host,
657 )
657 )
658 return encoding.strfromlocal(messageid)
658 return encoding.strfromlocal(messageid)
General Comments 0
You need to be logged in to leave comments. Login now