##// END OF EJS Templates
use ui.readsections in the notify extension
Alexis S. L. Carvalho -
r3434:bf10cd8b default
parent child Browse files
Show More
@@ -1,280 +1,280 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
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7 #
7 #
8 # hook extension to email notifications to people when changesets are
8 # hook extension to email notifications to people when changesets are
9 # committed to a repo they subscribe to.
9 # committed to a repo they subscribe to.
10 #
10 #
11 # default mode is to print messages to stdout, for testing and
11 # default mode is to print messages to stdout, for testing and
12 # configuring.
12 # configuring.
13 #
13 #
14 # to use, configure notify extension and enable in hgrc like this:
14 # to use, configure notify extension and enable in hgrc like this:
15 #
15 #
16 # [extensions]
16 # [extensions]
17 # hgext.notify =
17 # hgext.notify =
18 #
18 #
19 # [hooks]
19 # [hooks]
20 # # one email for each incoming changeset
20 # # one email for each incoming changeset
21 # incoming.notify = python:hgext.notify.hook
21 # incoming.notify = python:hgext.notify.hook
22 # # batch emails when many changesets incoming at one time
22 # # batch emails when many changesets incoming at one time
23 # changegroup.notify = python:hgext.notify.hook
23 # changegroup.notify = python:hgext.notify.hook
24 #
24 #
25 # [notify]
25 # [notify]
26 # # config items go in here
26 # # config items go in here
27 #
27 #
28 # config items:
28 # config items:
29 #
29 #
30 # REQUIRED:
30 # REQUIRED:
31 # config = /path/to/file # file containing subscriptions
31 # config = /path/to/file # file containing subscriptions
32 #
32 #
33 # OPTIONAL:
33 # OPTIONAL:
34 # test = True # print messages to stdout for testing
34 # test = True # print messages to stdout for testing
35 # strip = 3 # number of slashes to strip for url paths
35 # strip = 3 # number of slashes to strip for url paths
36 # domain = example.com # domain to use if committer missing domain
36 # domain = example.com # domain to use if committer missing domain
37 # style = ... # style file to use when formatting email
37 # style = ... # style file to use when formatting email
38 # template = ... # template to use when formatting email
38 # template = ... # template to use when formatting email
39 # incoming = ... # template to use when run as incoming hook
39 # incoming = ... # template to use when run as incoming hook
40 # changegroup = ... # template when run as changegroup hook
40 # changegroup = ... # template when run as changegroup hook
41 # maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
41 # maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
42 # maxsubject = 67 # truncate subject line longer than this
42 # maxsubject = 67 # truncate subject line longer than this
43 # diffstat = True # add a diffstat before the diff content
43 # diffstat = True # add a diffstat before the diff content
44 # sources = serve # notify if source of incoming changes in this list
44 # sources = serve # notify if source of incoming changes in this list
45 # # (serve == ssh or http, push, pull, bundle)
45 # # (serve == ssh or http, push, pull, bundle)
46 # [email]
46 # [email]
47 # from = user@host.com # email address to send as if none given
47 # from = user@host.com # email address to send as if none given
48 # [web]
48 # [web]
49 # baseurl = http://hgserver/... # root of hg web site for browsing commits
49 # baseurl = http://hgserver/... # root of hg web site for browsing commits
50 #
50 #
51 # notify config file has same format as regular hgrc. it has two
51 # notify config file has same format as regular hgrc. it has two
52 # sections so you can express subscriptions in whatever way is handier
52 # sections so you can express subscriptions in whatever way is handier
53 # for you.
53 # for you.
54 #
54 #
55 # [usersubs]
55 # [usersubs]
56 # # key is subscriber email, value is ","-separated list of glob patterns
56 # # key is subscriber email, value is ","-separated list of glob patterns
57 # user@host = pattern
57 # user@host = pattern
58 #
58 #
59 # [reposubs]
59 # [reposubs]
60 # # key is glob pattern, value is ","-separated list of subscriber emails
60 # # key is glob pattern, value is ","-separated list of subscriber emails
61 # pattern = user@host
61 # pattern = user@host
62 #
62 #
63 # glob patterns are matched against path to repo root.
63 # glob patterns are matched against path to repo root.
64 #
64 #
65 # if you like, you can put notify config file in repo that users can
65 # if you like, you can put notify config file in repo that users can
66 # push changes to, they can manage their own subscriptions.
66 # push changes to, they can manage their own subscriptions.
67
67
68 from mercurial.demandload import *
68 from mercurial.demandload import *
69 from mercurial.i18n import gettext as _
69 from mercurial.i18n import gettext as _
70 from mercurial.node import *
70 from mercurial.node import *
71 demandload(globals(), 'mercurial:commands,patch,templater,util,mail')
71 demandload(globals(), 'mercurial:commands,patch,templater,util,mail')
72 demandload(globals(), 'email.Parser fnmatch socket time')
72 demandload(globals(), 'email.Parser fnmatch socket time')
73
73
74 # template for single changeset can include email headers.
74 # template for single changeset can include email headers.
75 single_template = '''
75 single_template = '''
76 Subject: changeset in {webroot}: {desc|firstline|strip}
76 Subject: changeset in {webroot}: {desc|firstline|strip}
77 From: {author}
77 From: {author}
78
78
79 changeset {node|short} in {root}
79 changeset {node|short} in {root}
80 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
80 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
81 description:
81 description:
82 \t{desc|tabindent|strip}
82 \t{desc|tabindent|strip}
83 '''.lstrip()
83 '''.lstrip()
84
84
85 # template for multiple changesets should not contain email headers,
85 # template for multiple changesets should not contain email headers,
86 # because only first set of headers will be used and result will look
86 # because only first set of headers will be used and result will look
87 # strange.
87 # strange.
88 multiple_template = '''
88 multiple_template = '''
89 changeset {node|short} in {root}
89 changeset {node|short} in {root}
90 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
90 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
91 summary: {desc|firstline}
91 summary: {desc|firstline}
92 '''
92 '''
93
93
94 deftemplates = {
94 deftemplates = {
95 'changegroup': multiple_template,
95 'changegroup': multiple_template,
96 }
96 }
97
97
98 class notifier(object):
98 class notifier(object):
99 '''email notification class.'''
99 '''email notification class.'''
100
100
101 def __init__(self, ui, repo, hooktype):
101 def __init__(self, ui, repo, hooktype):
102 self.ui = ui
102 self.ui = ui
103 cfg = self.ui.config('notify', 'config')
103 cfg = self.ui.config('notify', 'config')
104 if cfg:
104 if cfg:
105 self.ui.readconfig(cfg)
105 self.ui.readsections(cfg, 'usersubs', 'reposubs')
106 self.repo = repo
106 self.repo = repo
107 self.stripcount = int(self.ui.config('notify', 'strip', 0))
107 self.stripcount = int(self.ui.config('notify', 'strip', 0))
108 self.root = self.strip(self.repo.root)
108 self.root = self.strip(self.repo.root)
109 self.domain = self.ui.config('notify', 'domain')
109 self.domain = self.ui.config('notify', 'domain')
110 self.sio = templater.stringio()
110 self.sio = templater.stringio()
111 self.subs = self.subscribers()
111 self.subs = self.subscribers()
112
112
113 mapfile = self.ui.config('notify', 'style')
113 mapfile = self.ui.config('notify', 'style')
114 template = (self.ui.config('notify', hooktype) or
114 template = (self.ui.config('notify', hooktype) or
115 self.ui.config('notify', 'template'))
115 self.ui.config('notify', 'template'))
116 self.t = templater.changeset_templater(self.ui, self.repo, mapfile,
116 self.t = templater.changeset_templater(self.ui, self.repo, mapfile,
117 self.sio)
117 self.sio)
118 if not mapfile and not template:
118 if not mapfile and not template:
119 template = deftemplates.get(hooktype) or single_template
119 template = deftemplates.get(hooktype) or single_template
120 if template:
120 if template:
121 template = templater.parsestring(template, quoted=False)
121 template = templater.parsestring(template, quoted=False)
122 self.t.use_template(template)
122 self.t.use_template(template)
123
123
124 def strip(self, path):
124 def strip(self, path):
125 '''strip leading slashes from local path, turn into web-safe path.'''
125 '''strip leading slashes from local path, turn into web-safe path.'''
126
126
127 path = util.pconvert(path)
127 path = util.pconvert(path)
128 count = self.stripcount
128 count = self.stripcount
129 while count > 0:
129 while count > 0:
130 c = path.find('/')
130 c = path.find('/')
131 if c == -1:
131 if c == -1:
132 break
132 break
133 path = path[c+1:]
133 path = path[c+1:]
134 count -= 1
134 count -= 1
135 return path
135 return path
136
136
137 def fixmail(self, addr):
137 def fixmail(self, addr):
138 '''try to clean up email addresses.'''
138 '''try to clean up email addresses.'''
139
139
140 addr = templater.email(addr.strip())
140 addr = templater.email(addr.strip())
141 a = addr.find('@localhost')
141 a = addr.find('@localhost')
142 if a != -1:
142 if a != -1:
143 addr = addr[:a]
143 addr = addr[:a]
144 if '@' not in addr:
144 if '@' not in addr:
145 return addr + '@' + self.domain
145 return addr + '@' + self.domain
146 return addr
146 return addr
147
147
148 def subscribers(self):
148 def subscribers(self):
149 '''return list of email addresses of subscribers to this repo.'''
149 '''return list of email addresses of subscribers to this repo.'''
150
150
151 subs = {}
151 subs = {}
152 for user, pats in self.ui.configitems('usersubs'):
152 for user, pats in self.ui.configitems('usersubs'):
153 for pat in pats.split(','):
153 for pat in pats.split(','):
154 if fnmatch.fnmatch(self.repo.root, pat.strip()):
154 if fnmatch.fnmatch(self.repo.root, pat.strip()):
155 subs[self.fixmail(user)] = 1
155 subs[self.fixmail(user)] = 1
156 for pat, users in self.ui.configitems('reposubs'):
156 for pat, users in self.ui.configitems('reposubs'):
157 if fnmatch.fnmatch(self.repo.root, pat):
157 if fnmatch.fnmatch(self.repo.root, pat):
158 for user in users.split(','):
158 for user in users.split(','):
159 subs[self.fixmail(user)] = 1
159 subs[self.fixmail(user)] = 1
160 subs = subs.keys()
160 subs = subs.keys()
161 subs.sort()
161 subs.sort()
162 return subs
162 return subs
163
163
164 def url(self, path=None):
164 def url(self, path=None):
165 return self.ui.config('web', 'baseurl') + (path or self.root)
165 return self.ui.config('web', 'baseurl') + (path or self.root)
166
166
167 def node(self, node):
167 def node(self, node):
168 '''format one changeset.'''
168 '''format one changeset.'''
169
169
170 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
170 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
171 baseurl=self.ui.config('web', 'baseurl'),
171 baseurl=self.ui.config('web', 'baseurl'),
172 root=self.repo.root,
172 root=self.repo.root,
173 webroot=self.root)
173 webroot=self.root)
174
174
175 def skipsource(self, source):
175 def skipsource(self, source):
176 '''true if incoming changes from this source should be skipped.'''
176 '''true if incoming changes from this source should be skipped.'''
177 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
177 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
178 return source not in ok_sources
178 return source not in ok_sources
179
179
180 def send(self, node, count):
180 def send(self, node, count):
181 '''send message.'''
181 '''send message.'''
182
182
183 p = email.Parser.Parser()
183 p = email.Parser.Parser()
184 self.sio.seek(0)
184 self.sio.seek(0)
185 msg = p.parse(self.sio)
185 msg = p.parse(self.sio)
186
186
187 def fix_subject():
187 def fix_subject():
188 '''try to make subject line exist and be useful.'''
188 '''try to make subject line exist and be useful.'''
189
189
190 subject = msg['Subject']
190 subject = msg['Subject']
191 if not subject:
191 if not subject:
192 if count > 1:
192 if count > 1:
193 subject = _('%s: %d new changesets') % (self.root, count)
193 subject = _('%s: %d new changesets') % (self.root, count)
194 else:
194 else:
195 changes = self.repo.changelog.read(node)
195 changes = self.repo.changelog.read(node)
196 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
196 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
197 subject = '%s: %s' % (self.root, s)
197 subject = '%s: %s' % (self.root, s)
198 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
198 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
199 if maxsubject and len(subject) > maxsubject:
199 if maxsubject and len(subject) > maxsubject:
200 subject = subject[:maxsubject-3] + '...'
200 subject = subject[:maxsubject-3] + '...'
201 del msg['Subject']
201 del msg['Subject']
202 msg['Subject'] = subject
202 msg['Subject'] = subject
203
203
204 def fix_sender():
204 def fix_sender():
205 '''try to make message have proper sender.'''
205 '''try to make message have proper sender.'''
206
206
207 sender = msg['From']
207 sender = msg['From']
208 if not sender:
208 if not sender:
209 sender = self.ui.config('email', 'from') or self.ui.username()
209 sender = self.ui.config('email', 'from') or self.ui.username()
210 if '@' not in sender or '@localhost' in sender:
210 if '@' not in sender or '@localhost' in sender:
211 sender = self.fixmail(sender)
211 sender = self.fixmail(sender)
212 del msg['From']
212 del msg['From']
213 msg['From'] = sender
213 msg['From'] = sender
214
214
215 fix_subject()
215 fix_subject()
216 fix_sender()
216 fix_sender()
217
217
218 msg['X-Hg-Notification'] = 'changeset ' + short(node)
218 msg['X-Hg-Notification'] = 'changeset ' + short(node)
219 if not msg['Message-Id']:
219 if not msg['Message-Id']:
220 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
220 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
221 (short(node), int(time.time()),
221 (short(node), int(time.time()),
222 hash(self.repo.root), socket.getfqdn()))
222 hash(self.repo.root), socket.getfqdn()))
223 msg['To'] = ', '.join(self.subs)
223 msg['To'] = ', '.join(self.subs)
224
224
225 msgtext = msg.as_string(0)
225 msgtext = msg.as_string(0)
226 if self.ui.configbool('notify', 'test', True):
226 if self.ui.configbool('notify', 'test', True):
227 self.ui.write(msgtext)
227 self.ui.write(msgtext)
228 if not msgtext.endswith('\n'):
228 if not msgtext.endswith('\n'):
229 self.ui.write('\n')
229 self.ui.write('\n')
230 else:
230 else:
231 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
231 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
232 (len(self.subs), count))
232 (len(self.subs), count))
233 mail.sendmail(self.ui, templater.email(msg['From']),
233 mail.sendmail(self.ui, templater.email(msg['From']),
234 self.subs, msgtext)
234 self.subs, msgtext)
235
235
236 def diff(self, node, ref):
236 def diff(self, node, ref):
237 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
237 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
238 if maxdiff == 0:
238 if maxdiff == 0:
239 return
239 return
240 fp = templater.stringio()
240 fp = templater.stringio()
241 prev = self.repo.changelog.parents(node)[0]
241 prev = self.repo.changelog.parents(node)[0]
242 patch.diff(self.repo, prev, ref, fp=fp)
242 patch.diff(self.repo, prev, ref, fp=fp)
243 difflines = fp.getvalue().splitlines(1)
243 difflines = fp.getvalue().splitlines(1)
244 if self.ui.configbool('notify', 'diffstat', True):
244 if self.ui.configbool('notify', 'diffstat', True):
245 s = patch.diffstat(difflines)
245 s = patch.diffstat(difflines)
246 self.sio.write('\ndiffstat:\n\n' + s)
246 self.sio.write('\ndiffstat:\n\n' + s)
247 if maxdiff > 0 and len(difflines) > maxdiff:
247 if maxdiff > 0 and len(difflines) > maxdiff:
248 self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
248 self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
249 (len(difflines), maxdiff))
249 (len(difflines), maxdiff))
250 difflines = difflines[:maxdiff]
250 difflines = difflines[:maxdiff]
251 elif difflines:
251 elif difflines:
252 self.sio.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
252 self.sio.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
253 self.sio.write(*difflines)
253 self.sio.write(*difflines)
254
254
255 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
255 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
256 '''send email notifications to interested subscribers.
256 '''send email notifications to interested subscribers.
257
257
258 if used as changegroup hook, send one email for all changesets in
258 if used as changegroup hook, send one email for all changesets in
259 changegroup. else send one email per changeset.'''
259 changegroup. else send one email per changeset.'''
260 n = notifier(ui, repo, hooktype)
260 n = notifier(ui, repo, hooktype)
261 if not n.subs:
261 if not n.subs:
262 ui.debug(_('notify: no subscribers to repo %s\n' % n.root))
262 ui.debug(_('notify: no subscribers to repo %s\n' % n.root))
263 return
263 return
264 if n.skipsource(source):
264 if n.skipsource(source):
265 ui.debug(_('notify: changes have source "%s" - skipping\n') %
265 ui.debug(_('notify: changes have source "%s" - skipping\n') %
266 source)
266 source)
267 return
267 return
268 node = bin(node)
268 node = bin(node)
269 if hooktype == 'changegroup':
269 if hooktype == 'changegroup':
270 start = repo.changelog.rev(node)
270 start = repo.changelog.rev(node)
271 end = repo.changelog.count()
271 end = repo.changelog.count()
272 count = end - start
272 count = end - start
273 for rev in xrange(start, end):
273 for rev in xrange(start, end):
274 n.node(repo.changelog.node(rev))
274 n.node(repo.changelog.node(rev))
275 n.diff(node, repo.changelog.tip())
275 n.diff(node, repo.changelog.tip())
276 else:
276 else:
277 count = 1
277 count = 1
278 n.node(node)
278 n.node(node)
279 n.diff(node, node)
279 n.diff(node, node)
280 n.send(node, count)
280 n.send(node, count)
General Comments 0
You need to be logged in to leave comments. Login now