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