##// END OF EJS Templates
notify: use absolute_import
timeless -
r28416:d3c8183f default
parent child Browse files
Show More
@@ -1,418 +1,428 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
85
86 notify.style
86 notify.style
87 Style file to use when formatting emails.
87 Style file to use when formatting emails.
88
88
89 notify.template
89 notify.template
90 Template to use when formatting emails.
90 Template to use when formatting emails.
91
91
92 notify.incoming
92 notify.incoming
93 Template to use when run as an incoming hook, overriding ``notify.template``.
93 Template to use when run as an incoming hook, overriding ``notify.template``.
94
94
95 notify.outgoing
95 notify.outgoing
96 Template to use when run as an outgoing hook, overriding ``notify.template``.
96 Template to use when run as an outgoing hook, overriding ``notify.template``.
97
97
98 notify.changegroup
98 notify.changegroup
99 Template to use when running as a changegroup hook, overriding
99 Template to use when running as a changegroup hook, overriding
100 ``notify.template``.
100 ``notify.template``.
101
101
102 notify.maxdiff
102 notify.maxdiff
103 Maximum number of diff lines to include in notification email. Set to 0
103 Maximum number of diff lines to include in notification email. Set to 0
104 to disable the diff, or -1 to include all of it. Default: 300.
104 to disable the diff, or -1 to include all of it. Default: 300.
105
105
106 notify.maxsubject
106 notify.maxsubject
107 Maximum number of characters in email's subject line. Default: 67.
107 Maximum number of characters in email's subject line. Default: 67.
108
108
109 notify.diffstat
109 notify.diffstat
110 Set to True to include a diffstat before diff content. Default: True.
110 Set to True to include a diffstat before diff content. Default: True.
111
111
112 notify.merge
112 notify.merge
113 If True, send notifications for merge changesets. Default: True.
113 If True, send notifications for merge changesets. Default: True.
114
114
115 notify.mbox
115 notify.mbox
116 If set, append mails to this mbox file instead of sending. Default: None.
116 If set, append mails to this mbox file instead of sending. Default: None.
117
117
118 notify.fromauthor
118 notify.fromauthor
119 If set, use the committer of the first changeset in a changegroup for
119 If set, use the committer of the first changeset in a changegroup for
120 the "From" field of the notification mail. If not set, take the user
120 the "From" field of the notification mail. If not set, take the user
121 from the pushing repo. Default: False.
121 from the pushing repo. Default: False.
122
122
123 If set, the following entries will also be used to customize the
123 If set, the following entries will also be used to customize the
124 notifications:
124 notifications:
125
125
126 email.from
126 email.from
127 Email ``From`` address to use if none can be found in the generated
127 Email ``From`` address to use if none can be found in the generated
128 email content.
128 email content.
129
129
130 web.baseurl
130 web.baseurl
131 Root repository URL to combine with repository paths when making
131 Root repository URL to combine with repository paths when making
132 references. See also ``notify.strip``.
132 references. See also ``notify.strip``.
133
133
134 '''
134 '''
135 from __future__ import absolute_import
135
136
136 import email, socket, time
137 import email
138 import fnmatch
139 import socket
140 import time
141
142 from mercurial import (
143 cmdutil,
144 error,
145 mail,
146 patch,
147 util,
148 )
137 from mercurial.i18n import _
149 from mercurial.i18n import _
138 from mercurial import patch, cmdutil, util, mail, error
139 import fnmatch
140
150
141 # Note for extension authors: ONLY specify testedwith = 'internal' for
151 # Note for extension authors: ONLY specify testedwith = 'internal' for
142 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
152 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
143 # be specifying the version(s) of Mercurial they are tested with, or
153 # be specifying the version(s) of Mercurial they are tested with, or
144 # leave the attribute unspecified.
154 # leave the attribute unspecified.
145 testedwith = 'internal'
155 testedwith = 'internal'
146
156
147 # template for single changeset can include email headers.
157 # template for single changeset can include email headers.
148 single_template = '''
158 single_template = '''
149 Subject: changeset in {webroot}: {desc|firstline|strip}
159 Subject: changeset in {webroot}: {desc|firstline|strip}
150 From: {author}
160 From: {author}
151
161
152 changeset {node|short} in {root}
162 changeset {node|short} in {root}
153 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
163 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
154 description:
164 description:
155 \t{desc|tabindent|strip}
165 \t{desc|tabindent|strip}
156 '''.lstrip()
166 '''.lstrip()
157
167
158 # template for multiple changesets should not contain email headers,
168 # template for multiple changesets should not contain email headers,
159 # because only first set of headers will be used and result will look
169 # because only first set of headers will be used and result will look
160 # strange.
170 # strange.
161 multiple_template = '''
171 multiple_template = '''
162 changeset {node|short} in {root}
172 changeset {node|short} in {root}
163 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
173 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
164 summary: {desc|firstline}
174 summary: {desc|firstline}
165 '''
175 '''
166
176
167 deftemplates = {
177 deftemplates = {
168 'changegroup': multiple_template,
178 'changegroup': multiple_template,
169 }
179 }
170
180
171 class notifier(object):
181 class notifier(object):
172 '''email notification class.'''
182 '''email notification class.'''
173
183
174 def __init__(self, ui, repo, hooktype):
184 def __init__(self, ui, repo, hooktype):
175 self.ui = ui
185 self.ui = ui
176 cfg = self.ui.config('notify', 'config')
186 cfg = self.ui.config('notify', 'config')
177 if cfg:
187 if cfg:
178 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
188 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
179 self.repo = repo
189 self.repo = repo
180 self.stripcount = int(self.ui.config('notify', 'strip', 0))
190 self.stripcount = int(self.ui.config('notify', 'strip', 0))
181 self.root = self.strip(self.repo.root)
191 self.root = self.strip(self.repo.root)
182 self.domain = self.ui.config('notify', 'domain')
192 self.domain = self.ui.config('notify', 'domain')
183 self.mbox = self.ui.config('notify', 'mbox')
193 self.mbox = self.ui.config('notify', 'mbox')
184 self.test = self.ui.configbool('notify', 'test', True)
194 self.test = self.ui.configbool('notify', 'test', True)
185 self.charsets = mail._charsets(self.ui)
195 self.charsets = mail._charsets(self.ui)
186 self.subs = self.subscribers()
196 self.subs = self.subscribers()
187 self.merge = self.ui.configbool('notify', 'merge', True)
197 self.merge = self.ui.configbool('notify', 'merge', True)
188
198
189 mapfile = self.ui.config('notify', 'style')
199 mapfile = self.ui.config('notify', 'style')
190 template = (self.ui.config('notify', hooktype) or
200 template = (self.ui.config('notify', hooktype) or
191 self.ui.config('notify', 'template'))
201 self.ui.config('notify', 'template'))
192 if not mapfile and not template:
202 if not mapfile and not template:
193 template = deftemplates.get(hooktype) or single_template
203 template = deftemplates.get(hooktype) or single_template
194 self.t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
204 self.t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
195 template, mapfile, False)
205 template, mapfile, False)
196
206
197 def strip(self, path):
207 def strip(self, path):
198 '''strip leading slashes from local path, turn into web-safe path.'''
208 '''strip leading slashes from local path, turn into web-safe path.'''
199
209
200 path = util.pconvert(path)
210 path = util.pconvert(path)
201 count = self.stripcount
211 count = self.stripcount
202 while count > 0:
212 while count > 0:
203 c = path.find('/')
213 c = path.find('/')
204 if c == -1:
214 if c == -1:
205 break
215 break
206 path = path[c + 1:]
216 path = path[c + 1:]
207 count -= 1
217 count -= 1
208 return path
218 return path
209
219
210 def fixmail(self, addr):
220 def fixmail(self, addr):
211 '''try to clean up email addresses.'''
221 '''try to clean up email addresses.'''
212
222
213 addr = util.email(addr.strip())
223 addr = util.email(addr.strip())
214 if self.domain:
224 if self.domain:
215 a = addr.find('@localhost')
225 a = addr.find('@localhost')
216 if a != -1:
226 if a != -1:
217 addr = addr[:a]
227 addr = addr[:a]
218 if '@' not in addr:
228 if '@' not in addr:
219 return addr + '@' + self.domain
229 return addr + '@' + self.domain
220 return addr
230 return addr
221
231
222 def subscribers(self):
232 def subscribers(self):
223 '''return list of email addresses of subscribers to this repo.'''
233 '''return list of email addresses of subscribers to this repo.'''
224 subs = set()
234 subs = set()
225 for user, pats in self.ui.configitems('usersubs'):
235 for user, pats in self.ui.configitems('usersubs'):
226 for pat in pats.split(','):
236 for pat in pats.split(','):
227 if '#' in pat:
237 if '#' in pat:
228 pat, revs = pat.split('#', 1)
238 pat, revs = pat.split('#', 1)
229 else:
239 else:
230 revs = None
240 revs = None
231 if fnmatch.fnmatch(self.repo.root, pat.strip()):
241 if fnmatch.fnmatch(self.repo.root, pat.strip()):
232 subs.add((self.fixmail(user), revs))
242 subs.add((self.fixmail(user), revs))
233 for pat, users in self.ui.configitems('reposubs'):
243 for pat, users in self.ui.configitems('reposubs'):
234 if '#' in pat:
244 if '#' in pat:
235 pat, revs = pat.split('#', 1)
245 pat, revs = pat.split('#', 1)
236 else:
246 else:
237 revs = None
247 revs = None
238 if fnmatch.fnmatch(self.repo.root, pat):
248 if fnmatch.fnmatch(self.repo.root, pat):
239 for user in users.split(','):
249 for user in users.split(','):
240 subs.add((self.fixmail(user), revs))
250 subs.add((self.fixmail(user), revs))
241 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
251 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
242 for s, r in sorted(subs)]
252 for s, r in sorted(subs)]
243
253
244 def node(self, ctx, **props):
254 def node(self, ctx, **props):
245 '''format one changeset, unless it is a suppressed merge.'''
255 '''format one changeset, unless it is a suppressed merge.'''
246 if not self.merge and len(ctx.parents()) > 1:
256 if not self.merge and len(ctx.parents()) > 1:
247 return False
257 return False
248 self.t.show(ctx, changes=ctx.changeset(),
258 self.t.show(ctx, changes=ctx.changeset(),
249 baseurl=self.ui.config('web', 'baseurl'),
259 baseurl=self.ui.config('web', 'baseurl'),
250 root=self.repo.root, webroot=self.root, **props)
260 root=self.repo.root, webroot=self.root, **props)
251 return True
261 return True
252
262
253 def skipsource(self, source):
263 def skipsource(self, source):
254 '''true if incoming changes from this source should be skipped.'''
264 '''true if incoming changes from this source should be skipped.'''
255 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
265 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
256 return source not in ok_sources
266 return source not in ok_sources
257
267
258 def send(self, ctx, count, data):
268 def send(self, ctx, count, data):
259 '''send message.'''
269 '''send message.'''
260
270
261 # Select subscribers by revset
271 # Select subscribers by revset
262 subs = set()
272 subs = set()
263 for sub, spec in self.subs:
273 for sub, spec in self.subs:
264 if spec is None:
274 if spec is None:
265 subs.add(sub)
275 subs.add(sub)
266 continue
276 continue
267 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
277 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
268 if len(revs):
278 if len(revs):
269 subs.add(sub)
279 subs.add(sub)
270 continue
280 continue
271 if len(subs) == 0:
281 if len(subs) == 0:
272 self.ui.debug('notify: no subscribers to selected repo '
282 self.ui.debug('notify: no subscribers to selected repo '
273 'and revset\n')
283 'and revset\n')
274 return
284 return
275
285
276 p = email.Parser.Parser()
286 p = email.Parser.Parser()
277 try:
287 try:
278 msg = p.parsestr(data)
288 msg = p.parsestr(data)
279 except email.Errors.MessageParseError as inst:
289 except email.Errors.MessageParseError as inst:
280 raise error.Abort(inst)
290 raise error.Abort(inst)
281
291
282 # store sender and subject
292 # store sender and subject
283 sender, subject = msg['From'], msg['Subject']
293 sender, subject = msg['From'], msg['Subject']
284 del msg['From'], msg['Subject']
294 del msg['From'], msg['Subject']
285
295
286 if not msg.is_multipart():
296 if not msg.is_multipart():
287 # create fresh mime message from scratch
297 # create fresh mime message from scratch
288 # (multipart templates must take care of this themselves)
298 # (multipart templates must take care of this themselves)
289 headers = msg.items()
299 headers = msg.items()
290 payload = msg.get_payload()
300 payload = msg.get_payload()
291 # for notification prefer readability over data precision
301 # for notification prefer readability over data precision
292 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
302 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
293 # reinstate custom headers
303 # reinstate custom headers
294 for k, v in headers:
304 for k, v in headers:
295 msg[k] = v
305 msg[k] = v
296
306
297 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
307 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
298
308
299 # try to make subject line exist and be useful
309 # try to make subject line exist and be useful
300 if not subject:
310 if not subject:
301 if count > 1:
311 if count > 1:
302 subject = _('%s: %d new changesets') % (self.root, count)
312 subject = _('%s: %d new changesets') % (self.root, count)
303 else:
313 else:
304 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
314 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
305 subject = '%s: %s' % (self.root, s)
315 subject = '%s: %s' % (self.root, s)
306 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
316 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
307 if maxsubject:
317 if maxsubject:
308 subject = util.ellipsis(subject, maxsubject)
318 subject = util.ellipsis(subject, maxsubject)
309 msg['Subject'] = mail.headencode(self.ui, subject,
319 msg['Subject'] = mail.headencode(self.ui, subject,
310 self.charsets, self.test)
320 self.charsets, self.test)
311
321
312 # try to make message have proper sender
322 # try to make message have proper sender
313 if not sender:
323 if not sender:
314 sender = self.ui.config('email', 'from') or self.ui.username()
324 sender = self.ui.config('email', 'from') or self.ui.username()
315 if '@' not in sender or '@localhost' in sender:
325 if '@' not in sender or '@localhost' in sender:
316 sender = self.fixmail(sender)
326 sender = self.fixmail(sender)
317 msg['From'] = mail.addressencode(self.ui, sender,
327 msg['From'] = mail.addressencode(self.ui, sender,
318 self.charsets, self.test)
328 self.charsets, self.test)
319
329
320 msg['X-Hg-Notification'] = 'changeset %s' % ctx
330 msg['X-Hg-Notification'] = 'changeset %s' % ctx
321 if not msg['Message-Id']:
331 if not msg['Message-Id']:
322 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
332 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
323 (ctx, int(time.time()),
333 (ctx, int(time.time()),
324 hash(self.repo.root), socket.getfqdn()))
334 hash(self.repo.root), socket.getfqdn()))
325 msg['To'] = ', '.join(sorted(subs))
335 msg['To'] = ', '.join(sorted(subs))
326
336
327 msgtext = msg.as_string()
337 msgtext = msg.as_string()
328 if self.test:
338 if self.test:
329 self.ui.write(msgtext)
339 self.ui.write(msgtext)
330 if not msgtext.endswith('\n'):
340 if not msgtext.endswith('\n'):
331 self.ui.write('\n')
341 self.ui.write('\n')
332 else:
342 else:
333 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
343 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
334 (len(subs), count))
344 (len(subs), count))
335 mail.sendmail(self.ui, util.email(msg['From']),
345 mail.sendmail(self.ui, util.email(msg['From']),
336 subs, msgtext, mbox=self.mbox)
346 subs, msgtext, mbox=self.mbox)
337
347
338 def diff(self, ctx, ref=None):
348 def diff(self, ctx, ref=None):
339
349
340 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
350 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
341 prev = ctx.p1().node()
351 prev = ctx.p1().node()
342 if ref:
352 if ref:
343 ref = ref.node()
353 ref = ref.node()
344 else:
354 else:
345 ref = ctx.node()
355 ref = ctx.node()
346 chunks = patch.diff(self.repo, prev, ref,
356 chunks = patch.diff(self.repo, prev, ref,
347 opts=patch.diffallopts(self.ui))
357 opts=patch.diffallopts(self.ui))
348 difflines = ''.join(chunks).splitlines()
358 difflines = ''.join(chunks).splitlines()
349
359
350 if self.ui.configbool('notify', 'diffstat', True):
360 if self.ui.configbool('notify', 'diffstat', True):
351 s = patch.diffstat(difflines)
361 s = patch.diffstat(difflines)
352 # s may be nil, don't include the header if it is
362 # s may be nil, don't include the header if it is
353 if s:
363 if s:
354 self.ui.write('\ndiffstat:\n\n%s' % s)
364 self.ui.write('\ndiffstat:\n\n%s' % s)
355
365
356 if maxdiff == 0:
366 if maxdiff == 0:
357 return
367 return
358 elif maxdiff > 0 and len(difflines) > maxdiff:
368 elif maxdiff > 0 and len(difflines) > maxdiff:
359 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
369 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
360 self.ui.write(msg % (len(difflines), maxdiff))
370 self.ui.write(msg % (len(difflines), maxdiff))
361 difflines = difflines[:maxdiff]
371 difflines = difflines[:maxdiff]
362 elif difflines:
372 elif difflines:
363 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
373 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
364
374
365 self.ui.write("\n".join(difflines))
375 self.ui.write("\n".join(difflines))
366
376
367 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
377 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
368 '''send email notifications to interested subscribers.
378 '''send email notifications to interested subscribers.
369
379
370 if used as changegroup hook, send one email for all changesets in
380 if used as changegroup hook, send one email for all changesets in
371 changegroup. else send one email per changeset.'''
381 changegroup. else send one email per changeset.'''
372
382
373 n = notifier(ui, repo, hooktype)
383 n = notifier(ui, repo, hooktype)
374 ctx = repo[node]
384 ctx = repo[node]
375
385
376 if not n.subs:
386 if not n.subs:
377 ui.debug('notify: no subscribers to repository %s\n' % n.root)
387 ui.debug('notify: no subscribers to repository %s\n' % n.root)
378 return
388 return
379 if n.skipsource(source):
389 if n.skipsource(source):
380 ui.debug('notify: changes have source "%s" - skipping\n' % source)
390 ui.debug('notify: changes have source "%s" - skipping\n' % source)
381 return
391 return
382
392
383 ui.pushbuffer()
393 ui.pushbuffer()
384 data = ''
394 data = ''
385 count = 0
395 count = 0
386 author = ''
396 author = ''
387 if hooktype == 'changegroup' or hooktype == 'outgoing':
397 if hooktype == 'changegroup' or hooktype == 'outgoing':
388 start, end = ctx.rev(), len(repo)
398 start, end = ctx.rev(), len(repo)
389 for rev in xrange(start, end):
399 for rev in xrange(start, end):
390 if n.node(repo[rev]):
400 if n.node(repo[rev]):
391 count += 1
401 count += 1
392 if not author:
402 if not author:
393 author = repo[rev].user()
403 author = repo[rev].user()
394 else:
404 else:
395 data += ui.popbuffer()
405 data += ui.popbuffer()
396 ui.note(_('notify: suppressing notification for merge %d:%s\n')
406 ui.note(_('notify: suppressing notification for merge %d:%s\n')
397 % (rev, repo[rev].hex()[:12]))
407 % (rev, repo[rev].hex()[:12]))
398 ui.pushbuffer()
408 ui.pushbuffer()
399 if count:
409 if count:
400 n.diff(ctx, repo['tip'])
410 n.diff(ctx, repo['tip'])
401 else:
411 else:
402 if not n.node(ctx):
412 if not n.node(ctx):
403 ui.popbuffer()
413 ui.popbuffer()
404 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
414 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
405 (ctx.rev(), ctx.hex()[:12]))
415 (ctx.rev(), ctx.hex()[:12]))
406 return
416 return
407 count += 1
417 count += 1
408 n.diff(ctx)
418 n.diff(ctx)
409 if not author:
419 if not author:
410 author = ctx.user()
420 author = ctx.user()
411
421
412 data += ui.popbuffer()
422 data += ui.popbuffer()
413 fromauthor = ui.config('notify', 'fromauthor')
423 fromauthor = ui.config('notify', 'fromauthor')
414 if author and fromauthor:
424 if author and fromauthor:
415 data = '\n'.join(['From: %s' % author, data])
425 data = '\n'.join(['From: %s' % author, data])
416
426
417 if count:
427 if count:
418 n.send(ctx, count, data)
428 n.send(ctx, count, data)
@@ -1,131 +1,130 b''
1 #require test-repo
1 #require test-repo
2
2
3 $ cd "$TESTDIR"/..
3 $ cd "$TESTDIR"/..
4
4
5 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
5 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
6 contrib/check-code.py not using absolute_import
6 contrib/check-code.py not using absolute_import
7 contrib/check-code.py requires print_function
7 contrib/check-code.py requires print_function
8 contrib/debugshell.py not using absolute_import
8 contrib/debugshell.py not using absolute_import
9 contrib/import-checker.py not using absolute_import
9 contrib/import-checker.py not using absolute_import
10 contrib/import-checker.py requires print_function
10 contrib/import-checker.py requires print_function
11 contrib/memory.py not using absolute_import
11 contrib/memory.py not using absolute_import
12 contrib/perf.py not using absolute_import
12 contrib/perf.py not using absolute_import
13 contrib/python-hook-examples.py not using absolute_import
13 contrib/python-hook-examples.py not using absolute_import
14 contrib/revsetbenchmarks.py not using absolute_import
14 contrib/revsetbenchmarks.py not using absolute_import
15 contrib/revsetbenchmarks.py requires print_function
15 contrib/revsetbenchmarks.py requires print_function
16 contrib/showstack.py not using absolute_import
16 contrib/showstack.py not using absolute_import
17 contrib/synthrepo.py not using absolute_import
17 contrib/synthrepo.py not using absolute_import
18 contrib/win32/hgwebdir_wsgi.py not using absolute_import
18 contrib/win32/hgwebdir_wsgi.py not using absolute_import
19 doc/check-seclevel.py not using absolute_import
19 doc/check-seclevel.py not using absolute_import
20 doc/gendoc.py not using absolute_import
20 doc/gendoc.py not using absolute_import
21 doc/hgmanpage.py not using absolute_import
21 doc/hgmanpage.py not using absolute_import
22 hgext/__init__.py not using absolute_import
22 hgext/__init__.py not using absolute_import
23 hgext/color.py not using absolute_import
23 hgext/color.py not using absolute_import
24 hgext/eol.py not using absolute_import
24 hgext/eol.py not using absolute_import
25 hgext/extdiff.py not using absolute_import
25 hgext/extdiff.py not using absolute_import
26 hgext/factotum.py not using absolute_import
26 hgext/factotum.py not using absolute_import
27 hgext/fetch.py not using absolute_import
27 hgext/fetch.py not using absolute_import
28 hgext/gpg.py not using absolute_import
28 hgext/gpg.py not using absolute_import
29 hgext/graphlog.py not using absolute_import
29 hgext/graphlog.py not using absolute_import
30 hgext/hgcia.py not using absolute_import
30 hgext/hgcia.py not using absolute_import
31 hgext/hgk.py not using absolute_import
31 hgext/hgk.py not using absolute_import
32 hgext/highlight/__init__.py not using absolute_import
32 hgext/highlight/__init__.py not using absolute_import
33 hgext/highlight/highlight.py not using absolute_import
33 hgext/highlight/highlight.py not using absolute_import
34 hgext/histedit.py not using absolute_import
34 hgext/histedit.py not using absolute_import
35 hgext/largefiles/__init__.py not using absolute_import
35 hgext/largefiles/__init__.py not using absolute_import
36 hgext/largefiles/basestore.py not using absolute_import
36 hgext/largefiles/basestore.py not using absolute_import
37 hgext/largefiles/lfcommands.py not using absolute_import
37 hgext/largefiles/lfcommands.py not using absolute_import
38 hgext/largefiles/lfutil.py not using absolute_import
38 hgext/largefiles/lfutil.py not using absolute_import
39 hgext/largefiles/localstore.py not using absolute_import
39 hgext/largefiles/localstore.py not using absolute_import
40 hgext/largefiles/overrides.py not using absolute_import
40 hgext/largefiles/overrides.py not using absolute_import
41 hgext/largefiles/proto.py not using absolute_import
41 hgext/largefiles/proto.py not using absolute_import
42 hgext/largefiles/remotestore.py not using absolute_import
42 hgext/largefiles/remotestore.py not using absolute_import
43 hgext/largefiles/reposetup.py not using absolute_import
43 hgext/largefiles/reposetup.py not using absolute_import
44 hgext/largefiles/uisetup.py not using absolute_import
44 hgext/largefiles/uisetup.py not using absolute_import
45 hgext/largefiles/wirestore.py not using absolute_import
45 hgext/largefiles/wirestore.py not using absolute_import
46 hgext/mq.py not using absolute_import
46 hgext/mq.py not using absolute_import
47 hgext/notify.py not using absolute_import
48 hgext/rebase.py not using absolute_import
47 hgext/rebase.py not using absolute_import
49 hgext/share.py not using absolute_import
48 hgext/share.py not using absolute_import
50 hgext/transplant.py not using absolute_import
49 hgext/transplant.py not using absolute_import
51 hgext/win32mbcs.py not using absolute_import
50 hgext/win32mbcs.py not using absolute_import
52 hgext/win32text.py not using absolute_import
51 hgext/win32text.py not using absolute_import
53 i18n/check-translation.py not using absolute_import
52 i18n/check-translation.py not using absolute_import
54 i18n/polib.py not using absolute_import
53 i18n/polib.py not using absolute_import
55 setup.py not using absolute_import
54 setup.py not using absolute_import
56 tests/filterpyflakes.py requires print_function
55 tests/filterpyflakes.py requires print_function
57 tests/generate-working-copy-states.py requires print_function
56 tests/generate-working-copy-states.py requires print_function
58 tests/get-with-headers.py requires print_function
57 tests/get-with-headers.py requires print_function
59 tests/heredoctest.py requires print_function
58 tests/heredoctest.py requires print_function
60 tests/hypothesishelpers.py not using absolute_import
59 tests/hypothesishelpers.py not using absolute_import
61 tests/hypothesishelpers.py requires print_function
60 tests/hypothesishelpers.py requires print_function
62 tests/killdaemons.py not using absolute_import
61 tests/killdaemons.py not using absolute_import
63 tests/md5sum.py not using absolute_import
62 tests/md5sum.py not using absolute_import
64 tests/mockblackbox.py not using absolute_import
63 tests/mockblackbox.py not using absolute_import
65 tests/printenv.py not using absolute_import
64 tests/printenv.py not using absolute_import
66 tests/readlink.py not using absolute_import
65 tests/readlink.py not using absolute_import
67 tests/readlink.py requires print_function
66 tests/readlink.py requires print_function
68 tests/revlog-formatv0.py not using absolute_import
67 tests/revlog-formatv0.py not using absolute_import
69 tests/run-tests.py not using absolute_import
68 tests/run-tests.py not using absolute_import
70 tests/seq.py not using absolute_import
69 tests/seq.py not using absolute_import
71 tests/seq.py requires print_function
70 tests/seq.py requires print_function
72 tests/silenttestrunner.py not using absolute_import
71 tests/silenttestrunner.py not using absolute_import
73 tests/silenttestrunner.py requires print_function
72 tests/silenttestrunner.py requires print_function
74 tests/sitecustomize.py not using absolute_import
73 tests/sitecustomize.py not using absolute_import
75 tests/svn-safe-append.py not using absolute_import
74 tests/svn-safe-append.py not using absolute_import
76 tests/svnxml.py not using absolute_import
75 tests/svnxml.py not using absolute_import
77 tests/test-ancestor.py requires print_function
76 tests/test-ancestor.py requires print_function
78 tests/test-atomictempfile.py not using absolute_import
77 tests/test-atomictempfile.py not using absolute_import
79 tests/test-batching.py not using absolute_import
78 tests/test-batching.py not using absolute_import
80 tests/test-batching.py requires print_function
79 tests/test-batching.py requires print_function
81 tests/test-bdiff.py not using absolute_import
80 tests/test-bdiff.py not using absolute_import
82 tests/test-bdiff.py requires print_function
81 tests/test-bdiff.py requires print_function
83 tests/test-context.py not using absolute_import
82 tests/test-context.py not using absolute_import
84 tests/test-context.py requires print_function
83 tests/test-context.py requires print_function
85 tests/test-demandimport.py not using absolute_import
84 tests/test-demandimport.py not using absolute_import
86 tests/test-demandimport.py requires print_function
85 tests/test-demandimport.py requires print_function
87 tests/test-doctest.py not using absolute_import
86 tests/test-doctest.py not using absolute_import
88 tests/test-duplicateoptions.py not using absolute_import
87 tests/test-duplicateoptions.py not using absolute_import
89 tests/test-duplicateoptions.py requires print_function
88 tests/test-duplicateoptions.py requires print_function
90 tests/test-filecache.py not using absolute_import
89 tests/test-filecache.py not using absolute_import
91 tests/test-filecache.py requires print_function
90 tests/test-filecache.py requires print_function
92 tests/test-filelog.py not using absolute_import
91 tests/test-filelog.py not using absolute_import
93 tests/test-filelog.py requires print_function
92 tests/test-filelog.py requires print_function
94 tests/test-hg-parseurl.py not using absolute_import
93 tests/test-hg-parseurl.py not using absolute_import
95 tests/test-hg-parseurl.py requires print_function
94 tests/test-hg-parseurl.py requires print_function
96 tests/test-hgweb-auth.py not using absolute_import
95 tests/test-hgweb-auth.py not using absolute_import
97 tests/test-hgweb-auth.py requires print_function
96 tests/test-hgweb-auth.py requires print_function
98 tests/test-hgwebdir-paths.py not using absolute_import
97 tests/test-hgwebdir-paths.py not using absolute_import
99 tests/test-hybridencode.py not using absolute_import
98 tests/test-hybridencode.py not using absolute_import
100 tests/test-hybridencode.py requires print_function
99 tests/test-hybridencode.py requires print_function
101 tests/test-lrucachedict.py not using absolute_import
100 tests/test-lrucachedict.py not using absolute_import
102 tests/test-lrucachedict.py requires print_function
101 tests/test-lrucachedict.py requires print_function
103 tests/test-manifest.py not using absolute_import
102 tests/test-manifest.py not using absolute_import
104 tests/test-minirst.py not using absolute_import
103 tests/test-minirst.py not using absolute_import
105 tests/test-minirst.py requires print_function
104 tests/test-minirst.py requires print_function
106 tests/test-parseindex2.py not using absolute_import
105 tests/test-parseindex2.py not using absolute_import
107 tests/test-parseindex2.py requires print_function
106 tests/test-parseindex2.py requires print_function
108 tests/test-pathencode.py not using absolute_import
107 tests/test-pathencode.py not using absolute_import
109 tests/test-pathencode.py requires print_function
108 tests/test-pathencode.py requires print_function
110 tests/test-propertycache.py not using absolute_import
109 tests/test-propertycache.py not using absolute_import
111 tests/test-propertycache.py requires print_function
110 tests/test-propertycache.py requires print_function
112 tests/test-revlog-ancestry.py not using absolute_import
111 tests/test-revlog-ancestry.py not using absolute_import
113 tests/test-revlog-ancestry.py requires print_function
112 tests/test-revlog-ancestry.py requires print_function
114 tests/test-run-tests.py not using absolute_import
113 tests/test-run-tests.py not using absolute_import
115 tests/test-simplemerge.py not using absolute_import
114 tests/test-simplemerge.py not using absolute_import
116 tests/test-status-inprocess.py not using absolute_import
115 tests/test-status-inprocess.py not using absolute_import
117 tests/test-status-inprocess.py requires print_function
116 tests/test-status-inprocess.py requires print_function
118 tests/test-symlink-os-yes-fs-no.py not using absolute_import
117 tests/test-symlink-os-yes-fs-no.py not using absolute_import
119 tests/test-trusted.py not using absolute_import
118 tests/test-trusted.py not using absolute_import
120 tests/test-trusted.py requires print_function
119 tests/test-trusted.py requires print_function
121 tests/test-ui-color.py not using absolute_import
120 tests/test-ui-color.py not using absolute_import
122 tests/test-ui-color.py requires print_function
121 tests/test-ui-color.py requires print_function
123 tests/test-ui-config.py not using absolute_import
122 tests/test-ui-config.py not using absolute_import
124 tests/test-ui-config.py requires print_function
123 tests/test-ui-config.py requires print_function
125 tests/test-ui-verbosity.py not using absolute_import
124 tests/test-ui-verbosity.py not using absolute_import
126 tests/test-ui-verbosity.py requires print_function
125 tests/test-ui-verbosity.py requires print_function
127 tests/test-url.py not using absolute_import
126 tests/test-url.py not using absolute_import
128 tests/test-url.py requires print_function
127 tests/test-url.py requires print_function
129 tests/test-walkrepo.py requires print_function
128 tests/test-walkrepo.py requires print_function
130 tests/test-wireproto.py requires print_function
129 tests/test-wireproto.py requires print_function
131 tests/tinyproxy.py requires print_function
130 tests/tinyproxy.py requires print_function
General Comments 0
You need to be logged in to leave comments. Login now