##// END OF EJS Templates
merge with crew-stable
Dirkjan Ochtman -
r9316:23cf7b52 merge default
parent child Browse files
Show More
@@ -1,293 +1,298 b''
1 1 # notify.py - email notifications for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 '''hooks for sending email notifications at commit/push time
9 9
10 10 Subscriptions can be managed through a hgrc file. Default mode is to
11 11 print messages to stdout, for testing and configuring.
12 12
13 13 To use, configure the notify extension and enable it in hgrc like
14 14 this::
15 15
16 16 [extensions]
17 17 hgext.notify =
18 18
19 19 [hooks]
20 20 # one email for each incoming changeset
21 21 incoming.notify = python:hgext.notify.hook
22 22 # batch emails when many changesets incoming at one time
23 23 changegroup.notify = python:hgext.notify.hook
24 24
25 25 [notify]
26 26 # config items go here
27 27
28 28 Required configuration items::
29 29
30 30 config = /path/to/file # file containing subscriptions
31 31
32 32 Optional configuration items::
33 33
34 34 test = True # print messages to stdout for testing
35 35 strip = 3 # number of slashes to strip for url paths
36 36 domain = example.com # domain to use if committer missing domain
37 37 style = ... # style file to use when formatting email
38 38 template = ... # template to use when formatting email
39 39 incoming = ... # template to use when run as incoming hook
40 40 changegroup = ... # template when run as changegroup hook
41 41 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
42 42 maxsubject = 67 # truncate subject line longer than this
43 43 diffstat = True # add a diffstat before the diff content
44 44 sources = serve # notify if source of incoming changes in this list
45 45 # (serve == ssh or http, push, pull, bundle)
46 46 [email]
47 47 from = user@host.com # email address to send as if none given
48 48 [web]
49 49 baseurl = http://hgserver/... # root of hg web site for browsing commits
50 50
51 51 The notify config file has same format as a regular hgrc file. It has
52 52 two sections so you can express subscriptions in whatever way is
53 53 handier for you.
54 54
55 55 ::
56 56
57 57 [usersubs]
58 58 # key is subscriber email, value is ","-separated list of glob patterns
59 59 user@host = pattern
60 60
61 61 [reposubs]
62 62 # key is glob pattern, value is ","-separated list of subscriber emails
63 63 pattern = user@host
64 64
65 65 Glob patterns are matched against path to repository root.
66 66
67 67 If you like, you can put notify config file in repository that users
68 68 can push changes to, they can manage their own subscriptions.
69 69 '''
70 70
71 71 from mercurial.i18n import _
72 72 from mercurial import patch, cmdutil, templater, util, mail
73 import email.Parser, fnmatch, socket, time
73 import email.Parser, email.Errors, fnmatch, socket, time
74 74
75 75 # template for single changeset can include email headers.
76 76 single_template = '''
77 77 Subject: changeset in {webroot}: {desc|firstline|strip}
78 78 From: {author}
79 79
80 80 changeset {node|short} in {root}
81 81 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
82 82 description:
83 83 \t{desc|tabindent|strip}
84 84 '''.lstrip()
85 85
86 86 # template for multiple changesets should not contain email headers,
87 87 # because only first set of headers will be used and result will look
88 88 # strange.
89 89 multiple_template = '''
90 90 changeset {node|short} in {root}
91 91 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
92 92 summary: {desc|firstline}
93 93 '''
94 94
95 95 deftemplates = {
96 96 'changegroup': multiple_template,
97 97 }
98 98
99 99 class notifier(object):
100 100 '''email notification class.'''
101 101
102 102 def __init__(self, ui, repo, hooktype):
103 103 self.ui = ui
104 104 cfg = self.ui.config('notify', 'config')
105 105 if cfg:
106 106 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
107 107 self.repo = repo
108 108 self.stripcount = int(self.ui.config('notify', 'strip', 0))
109 109 self.root = self.strip(self.repo.root)
110 110 self.domain = self.ui.config('notify', 'domain')
111 111 self.test = self.ui.configbool('notify', 'test', True)
112 112 self.charsets = mail._charsets(self.ui)
113 113 self.subs = self.subscribers()
114 114
115 115 mapfile = self.ui.config('notify', 'style')
116 116 template = (self.ui.config('notify', hooktype) or
117 117 self.ui.config('notify', 'template'))
118 118 self.t = cmdutil.changeset_templater(self.ui, self.repo,
119 119 False, None, mapfile, False)
120 120 if not mapfile and not template:
121 121 template = deftemplates.get(hooktype) or single_template
122 122 if template:
123 123 template = templater.parsestring(template, quoted=False)
124 124 self.t.use_template(template)
125 125
126 126 def strip(self, path):
127 127 '''strip leading slashes from local path, turn into web-safe path.'''
128 128
129 129 path = util.pconvert(path)
130 130 count = self.stripcount
131 131 while count > 0:
132 132 c = path.find('/')
133 133 if c == -1:
134 134 break
135 135 path = path[c+1:]
136 136 count -= 1
137 137 return path
138 138
139 139 def fixmail(self, addr):
140 140 '''try to clean up email addresses.'''
141 141
142 142 addr = util.email(addr.strip())
143 143 if self.domain:
144 144 a = addr.find('@localhost')
145 145 if a != -1:
146 146 addr = addr[:a]
147 147 if '@' not in addr:
148 148 return addr + '@' + self.domain
149 149 return addr
150 150
151 151 def subscribers(self):
152 152 '''return list of email addresses of subscribers to this repo.'''
153 153 subs = set()
154 154 for user, pats in self.ui.configitems('usersubs'):
155 155 for pat in pats.split(','):
156 156 if fnmatch.fnmatch(self.repo.root, pat.strip()):
157 157 subs.add(self.fixmail(user))
158 158 for pat, users in self.ui.configitems('reposubs'):
159 159 if fnmatch.fnmatch(self.repo.root, pat):
160 160 for user in users.split(','):
161 161 subs.add(self.fixmail(user))
162 162 return [mail.addressencode(self.ui, s, self.charsets, self.test)
163 163 for s in sorted(subs)]
164 164
165 165 def url(self, path=None):
166 166 return self.ui.config('web', 'baseurl') + (path or self.root)
167 167
168 168 def node(self, ctx):
169 169 '''format one changeset.'''
170 170 self.t.show(ctx, changes=ctx.changeset(),
171 171 baseurl=self.ui.config('web', 'baseurl'),
172 172 root=self.repo.root, webroot=self.root)
173 173
174 174 def skipsource(self, source):
175 175 '''true if incoming changes from this source should be skipped.'''
176 176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
177 177 return source not in ok_sources
178 178
179 179 def send(self, ctx, count, data):
180 180 '''send message.'''
181 181
182 182 p = email.Parser.Parser()
183 msg = p.parsestr(data)
183 try:
184 msg = p.parsestr(data)
185 except email.Errors.MessageParseError, inst:
186 raise util.Abort(inst)
184 187
185 188 # store sender and subject
186 189 sender, subject = msg['From'], msg['Subject']
187 190 del msg['From'], msg['Subject']
188 # store remaining headers
189 headers = msg.items()
190 # create fresh mime message from msg body
191 text = msg.get_payload()
192 # for notification prefer readability over data precision
193 msg = mail.mimeencode(self.ui, text, self.charsets, self.test)
194 # reinstate custom headers
195 for k, v in headers:
196 msg[k] = v
191
192 if not msg.is_multipart():
193 # create fresh mime message from scratch
194 # (multipart templates must take care of this themselves)
195 headers = msg.items()
196 payload = msg.get_payload()
197 # for notification prefer readability over data precision
198 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
199 # reinstate custom headers
200 for k, v in headers:
201 msg[k] = v
197 202
198 203 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
199 204
200 205 # try to make subject line exist and be useful
201 206 if not subject:
202 207 if count > 1:
203 208 subject = _('%s: %d new changesets') % (self.root, count)
204 209 else:
205 210 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
206 211 subject = '%s: %s' % (self.root, s)
207 212 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
208 213 if maxsubject and len(subject) > maxsubject:
209 214 subject = subject[:maxsubject-3] + '...'
210 215 msg['Subject'] = mail.headencode(self.ui, subject,
211 216 self.charsets, self.test)
212 217
213 218 # try to make message have proper sender
214 219 if not sender:
215 220 sender = self.ui.config('email', 'from') or self.ui.username()
216 221 if '@' not in sender or '@localhost' in sender:
217 222 sender = self.fixmail(sender)
218 223 msg['From'] = mail.addressencode(self.ui, sender,
219 224 self.charsets, self.test)
220 225
221 226 msg['X-Hg-Notification'] = 'changeset %s' % ctx
222 227 if not msg['Message-Id']:
223 228 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
224 229 (ctx, int(time.time()),
225 230 hash(self.repo.root), socket.getfqdn()))
226 231 msg['To'] = ', '.join(self.subs)
227 232
228 233 msgtext = msg.as_string()
229 234 if self.test:
230 235 self.ui.write(msgtext)
231 236 if not msgtext.endswith('\n'):
232 237 self.ui.write('\n')
233 238 else:
234 239 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
235 240 (len(self.subs), count))
236 241 mail.sendmail(self.ui, util.email(msg['From']),
237 242 self.subs, msgtext)
238 243
239 244 def diff(self, ctx, ref=None):
240 245
241 246 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
242 247 prev = ctx.parents()[0].node()
243 248 ref = ref and ref.node() or ctx.node()
244 249 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
245 250 difflines = ''.join(chunks).splitlines()
246 251
247 252 if self.ui.configbool('notify', 'diffstat', True):
248 253 s = patch.diffstat(difflines)
249 254 # s may be nil, don't include the header if it is
250 255 if s:
251 256 self.ui.write('\ndiffstat:\n\n%s' % s)
252 257
253 258 if maxdiff == 0:
254 259 return
255 260 elif maxdiff > 0 and len(difflines) > maxdiff:
256 261 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
257 262 self.ui.write(msg % (len(difflines), maxdiff))
258 263 difflines = difflines[:maxdiff]
259 264 elif difflines:
260 265 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
261 266
262 267 self.ui.write("\n".join(difflines))
263 268
264 269 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
265 270 '''send email notifications to interested subscribers.
266 271
267 272 if used as changegroup hook, send one email for all changesets in
268 273 changegroup. else send one email per changeset.'''
269 274
270 275 n = notifier(ui, repo, hooktype)
271 276 ctx = repo[node]
272 277
273 278 if not n.subs:
274 279 ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
275 280 return
276 281 if n.skipsource(source):
277 282 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
278 283 return
279 284
280 285 ui.pushbuffer()
281 286 if hooktype == 'changegroup':
282 287 start, end = ctx.rev(), len(repo)
283 288 count = end - start
284 289 for rev in xrange(start, end):
285 290 n.node(repo[rev])
286 291 n.diff(ctx, repo['tip'])
287 292 else:
288 293 count = 1
289 294 n.node(ctx)
290 295 n.diff(ctx)
291 296
292 297 data = ui.popbuffer()
293 298 n.send(ctx, count, data)
@@ -1,136 +1,139 b''
1 1 # demandimport.py - global demand-loading of modules for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 '''
9 9 demandimport - automatic demandloading of modules
10 10
11 11 To enable this module, do:
12 12
13 13 import demandimport; demandimport.enable()
14 14
15 15 Imports of the following forms will be demand-loaded:
16 16
17 17 import a, b.c
18 18 import a.b as c
19 19 from a import b,c # a will be loaded immediately
20 20
21 21 These imports will not be delayed:
22 22
23 23 from a import *
24 24 b = __import__(a)
25 25 '''
26 26
27 27 import __builtin__
28 28 _origimport = __import__
29 29
30 30 class _demandmod(object):
31 31 """module demand-loader and proxy"""
32 32 def __init__(self, name, globals, locals):
33 33 if '.' in name:
34 34 head, rest = name.split('.', 1)
35 35 after = [rest]
36 36 else:
37 37 head = name
38 38 after = []
39 39 object.__setattr__(self, "_data", (head, globals, locals, after))
40 40 object.__setattr__(self, "_module", None)
41 41 def _extend(self, name):
42 42 """add to the list of submodules to load"""
43 43 self._data[3].append(name)
44 44 def _load(self):
45 45 if not self._module:
46 46 head, globals, locals, after = self._data
47 47 mod = _origimport(head, globals, locals)
48 48 # load submodules
49 49 def subload(mod, p):
50 50 h, t = p, None
51 51 if '.' in p:
52 52 h, t = p.split('.', 1)
53 53 if not hasattr(mod, h):
54 54 setattr(mod, h, _demandmod(p, mod.__dict__, mod.__dict__))
55 55 elif t:
56 56 subload(getattr(mod, h), t)
57 57
58 58 for x in after:
59 59 subload(mod, x)
60 60
61 61 # are we in the locals dictionary still?
62 62 if locals and locals.get(head) == self:
63 63 locals[head] = mod
64 64 object.__setattr__(self, "_module", mod)
65 65
66 66 def __repr__(self):
67 67 if self._module:
68 68 return "<proxied module '%s'>" % self._data[0]
69 69 return "<unloaded module '%s'>" % self._data[0]
70 70 def __call__(self, *args, **kwargs):
71 71 raise TypeError("%s object is not callable" % repr(self))
72 72 def __getattribute__(self, attr):
73 73 if attr in ('_data', '_extend', '_load', '_module'):
74 74 return object.__getattribute__(self, attr)
75 75 self._load()
76 76 return getattr(self._module, attr)
77 77 def __setattr__(self, attr, val):
78 78 self._load()
79 79 setattr(self._module, attr, val)
80 80
81 81 def _demandimport(name, globals=None, locals=None, fromlist=None, level=None):
82 82 if not locals or name in ignore or fromlist == ('*',):
83 83 # these cases we can't really delay
84 return _origimport(name, globals, locals, fromlist)
84 if level is None:
85 return _origimport(name, globals, locals, fromlist)
86 else:
87 return _origimport(name, globals, locals, fromlist, level)
85 88 elif not fromlist:
86 89 # import a [as b]
87 90 if '.' in name: # a.b
88 91 base, rest = name.split('.', 1)
89 92 # email.__init__ loading email.mime
90 93 if globals and globals.get('__name__', None) == base:
91 94 return _origimport(name, globals, locals, fromlist)
92 95 # if a is already demand-loaded, add b to its submodule list
93 96 if base in locals:
94 97 if isinstance(locals[base], _demandmod):
95 98 locals[base]._extend(rest)
96 99 return locals[base]
97 100 return _demandmod(name, globals, locals)
98 101 else:
99 102 if level is not None:
100 103 # from . import b,c,d or from .a import b,c,d
101 104 return _origimport(name, globals, locals, fromlist, level)
102 105 # from a import b,c,d
103 106 mod = _origimport(name, globals, locals)
104 107 # recurse down the module chain
105 108 for comp in name.split('.')[1:]:
106 109 if not hasattr(mod, comp):
107 110 setattr(mod, comp, _demandmod(comp, mod.__dict__, mod.__dict__))
108 111 mod = getattr(mod, comp)
109 112 for x in fromlist:
110 113 # set requested submodules for demand load
111 114 if not(hasattr(mod, x)):
112 115 setattr(mod, x, _demandmod(x, mod.__dict__, locals))
113 116 return mod
114 117
115 118 ignore = [
116 119 '_hashlib',
117 120 '_xmlplus',
118 121 'fcntl',
119 122 'win32com.gen_py',
120 123 'pythoncom',
121 124 # imported by tarfile, not available under Windows
122 125 'pwd',
123 126 'grp',
124 127 # imported by profile, itself imported by hotshot.stats,
125 128 # not available under Windows
126 129 'resource',
127 130 ]
128 131
129 132 def enable():
130 133 "enable global demand-loading of modules"
131 134 __builtin__.__import__ = _demandimport
132 135
133 136 def disable():
134 137 "disable global demand-loading of modules"
135 138 __builtin__.__import__ = _origimport
136 139
@@ -1,113 +1,113 b''
1 1 #! /usr/bin/env python
2 2
3 3 import sys
4 4 from _lsprof import Profiler, profiler_entry
5 5
6 6 __all__ = ['profile', 'Stats']
7 7
8 8 def profile(f, *args, **kwds):
9 9 """XXX docstring"""
10 10 p = Profiler()
11 11 p.enable(subcalls=True, builtins=True)
12 12 try:
13 13 f(*args, **kwds)
14 14 finally:
15 15 p.disable()
16 16 return Stats(p.getstats())
17 17
18 18
19 19 class Stats(object):
20 20 """XXX docstring"""
21 21
22 22 def __init__(self, data):
23 23 self.data = data
24 24
25 25 def sort(self, crit="inlinetime"):
26 26 """XXX docstring"""
27 27 if crit not in profiler_entry.__dict__:
28 28 raise ValueError("Can't sort by %s" % crit)
29 29 self.data.sort(key=lambda x: getattr(x, crit), reverse=True)
30 30 for e in self.data:
31 31 if e.calls:
32 32 e.calls.sort(key=lambda x: getattr(x, crit), reverse=True)
33 33
34 34 def pprint(self, top=None, file=None, limit=None, climit=None):
35 35 """XXX docstring"""
36 36 if file is None:
37 37 file = sys.stdout
38 38 d = self.data
39 39 if top is not None:
40 40 d = d[:top]
41 41 cols = "% 12s %12s %11.4f %11.4f %s\n"
42 42 hcols = "% 12s %12s %12s %12s %s\n"
43 43 file.write(hcols % ("CallCount", "Recursive", "Total(ms)",
44 44 "Inline(ms)", "module:lineno(function)"))
45 45 count = 0
46 46 for e in d:
47 47 file.write(cols % (e.callcount, e.reccallcount, e.totaltime,
48 48 e.inlinetime, label(e.code)))
49 49 count += 1
50 50 if limit is not None and count == limit:
51 51 return
52 52 ccount = 0
53 53 if e.calls:
54 54 for se in e.calls:
55 55 file.write(cols % ("+%s" % se.callcount, se.reccallcount,
56 56 se.totaltime, se.inlinetime,
57 57 "+%s" % label(se.code)))
58 58 count += 1
59 59 ccount += 1
60 60 if limit is not None and count == limit:
61 61 return
62 62 if climit is not None and ccount == climit:
63 63 break
64 64
65 65 def freeze(self):
66 66 """Replace all references to code objects with string
67 67 descriptions; this makes it possible to pickle the instance."""
68 68
69 69 # this code is probably rather ickier than it needs to be!
70 70 for i in range(len(self.data)):
71 71 e = self.data[i]
72 72 if not isinstance(e.code, str):
73 73 self.data[i] = type(e)((label(e.code),) + e[1:])
74 74 if e.calls:
75 75 for j in range(len(e.calls)):
76 76 se = e.calls[j]
77 77 if not isinstance(se.code, str):
78 78 e.calls[j] = type(se)((label(se.code),) + se[1:])
79 79
80 80 _fn2mod = {}
81 81
82 82 def label(code):
83 83 if isinstance(code, str):
84 84 return code
85 85 try:
86 86 mname = _fn2mod[code.co_filename]
87 87 except KeyError:
88 for k, v in sys.modules.iteritems():
88 for k, v in list(sys.modules.iteritems()):
89 89 if v is None:
90 90 continue
91 91 if not hasattr(v, '__file__'):
92 92 continue
93 93 if not isinstance(v.__file__, str):
94 94 continue
95 95 if v.__file__.startswith(code.co_filename):
96 96 mname = _fn2mod[code.co_filename] = k
97 97 break
98 98 else:
99 99 mname = _fn2mod[code.co_filename] = '<%s>'%code.co_filename
100 100
101 101 return '%s:%d(%s)' % (mname, code.co_firstlineno, code.co_name)
102 102
103 103
104 104 if __name__ == '__main__':
105 105 import os
106 106 sys.argv = sys.argv[1:]
107 107 if not sys.argv:
108 108 print >> sys.stderr, "usage: lsprof.py <script> <arguments...>"
109 109 sys.exit(2)
110 110 sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])))
111 111 stats = profile(execfile, sys.argv[0], globals(), locals())
112 112 stats.sort()
113 113 stats.pprint()
General Comments 0
You need to be logged in to leave comments. Login now