##// END OF EJS Templates
notify: make it possible to pass extra info into templates
Bryan O'Sullivan -
r9486:dd8d10c3 default
parent child Browse files
Show More
@@ -1,298 +1,298 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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''hooks for sending email notifications at commit/push time
8 '''hooks for sending email notifications at commit/push time
9
9
10 Subscriptions can be managed through a hgrc file. Default mode is to
10 Subscriptions can be managed through a hgrc file. Default mode is to
11 print messages to stdout, for testing and configuring.
11 print messages to stdout, for testing and configuring.
12
12
13 To use, configure the notify extension and enable it in hgrc like
13 To use, configure the notify extension and enable it in hgrc like
14 this::
14 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 here
26 # config items go here
27
27
28 Required configuration items::
28 Required configuration items::
29
29
30 config = /path/to/file # file containing subscriptions
30 config = /path/to/file # file containing subscriptions
31
31
32 Optional configuration items::
32 Optional configuration items::
33
33
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 The notify config file has same format as a regular hgrc file. It has
51 The notify config file has same format as a regular hgrc file. It has
52 two sections so you can express subscriptions in whatever way is
52 two sections so you can express subscriptions in whatever way is
53 handier for you.
53 handier for you.
54
54
55 ::
55 ::
56
56
57 [usersubs]
57 [usersubs]
58 # key is subscriber email, value is ","-separated list of glob patterns
58 # key is subscriber email, value is ","-separated list of glob patterns
59 user@host = pattern
59 user@host = pattern
60
60
61 [reposubs]
61 [reposubs]
62 # key is glob pattern, value is ","-separated list of subscriber emails
62 # key is glob pattern, value is ","-separated list of subscriber emails
63 pattern = user@host
63 pattern = user@host
64
64
65 Glob patterns are matched against path to repository root.
65 Glob patterns are matched against path to repository root.
66
66
67 If you like, you can put notify config file in repository that users
67 If you like, you can put notify config file in repository that users
68 can push changes to, they can manage their own subscriptions.
68 can push changes to, they can manage their own subscriptions.
69 '''
69 '''
70
70
71 from mercurial.i18n import _
71 from mercurial.i18n import _
72 from mercurial import patch, cmdutil, templater, util, mail
72 from mercurial import patch, cmdutil, templater, util, mail
73 import email.Parser, email.Errors, fnmatch, socket, time
73 import email.Parser, email.Errors, fnmatch, socket, time
74
74
75 # template for single changeset can include email headers.
75 # template for single changeset can include email headers.
76 single_template = '''
76 single_template = '''
77 Subject: changeset in {webroot}: {desc|firstline|strip}
77 Subject: changeset in {webroot}: {desc|firstline|strip}
78 From: {author}
78 From: {author}
79
79
80 changeset {node|short} in {root}
80 changeset {node|short} in {root}
81 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
81 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
82 description:
82 description:
83 \t{desc|tabindent|strip}
83 \t{desc|tabindent|strip}
84 '''.lstrip()
84 '''.lstrip()
85
85
86 # template for multiple changesets should not contain email headers,
86 # template for multiple changesets should not contain email headers,
87 # because only first set of headers will be used and result will look
87 # because only first set of headers will be used and result will look
88 # strange.
88 # strange.
89 multiple_template = '''
89 multiple_template = '''
90 changeset {node|short} in {root}
90 changeset {node|short} in {root}
91 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
91 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
92 summary: {desc|firstline}
92 summary: {desc|firstline}
93 '''
93 '''
94
94
95 deftemplates = {
95 deftemplates = {
96 'changegroup': multiple_template,
96 'changegroup': multiple_template,
97 }
97 }
98
98
99 class notifier(object):
99 class notifier(object):
100 '''email notification class.'''
100 '''email notification class.'''
101
101
102 def __init__(self, ui, repo, hooktype):
102 def __init__(self, ui, repo, hooktype):
103 self.ui = ui
103 self.ui = ui
104 cfg = self.ui.config('notify', 'config')
104 cfg = self.ui.config('notify', 'config')
105 if cfg:
105 if cfg:
106 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
106 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
107 self.repo = repo
107 self.repo = repo
108 self.stripcount = int(self.ui.config('notify', 'strip', 0))
108 self.stripcount = int(self.ui.config('notify', 'strip', 0))
109 self.root = self.strip(self.repo.root)
109 self.root = self.strip(self.repo.root)
110 self.domain = self.ui.config('notify', 'domain')
110 self.domain = self.ui.config('notify', 'domain')
111 self.test = self.ui.configbool('notify', 'test', True)
111 self.test = self.ui.configbool('notify', 'test', True)
112 self.charsets = mail._charsets(self.ui)
112 self.charsets = mail._charsets(self.ui)
113 self.subs = self.subscribers()
113 self.subs = self.subscribers()
114
114
115 mapfile = self.ui.config('notify', 'style')
115 mapfile = self.ui.config('notify', 'style')
116 template = (self.ui.config('notify', hooktype) or
116 template = (self.ui.config('notify', hooktype) or
117 self.ui.config('notify', 'template'))
117 self.ui.config('notify', 'template'))
118 self.t = cmdutil.changeset_templater(self.ui, self.repo,
118 self.t = cmdutil.changeset_templater(self.ui, self.repo,
119 False, None, mapfile, False)
119 False, None, mapfile, False)
120 if not mapfile and not template:
120 if not mapfile and not template:
121 template = deftemplates.get(hooktype) or single_template
121 template = deftemplates.get(hooktype) or single_template
122 if template:
122 if template:
123 template = templater.parsestring(template, quoted=False)
123 template = templater.parsestring(template, quoted=False)
124 self.t.use_template(template)
124 self.t.use_template(template)
125
125
126 def strip(self, path):
126 def strip(self, path):
127 '''strip leading slashes from local path, turn into web-safe path.'''
127 '''strip leading slashes from local path, turn into web-safe path.'''
128
128
129 path = util.pconvert(path)
129 path = util.pconvert(path)
130 count = self.stripcount
130 count = self.stripcount
131 while count > 0:
131 while count > 0:
132 c = path.find('/')
132 c = path.find('/')
133 if c == -1:
133 if c == -1:
134 break
134 break
135 path = path[c+1:]
135 path = path[c+1:]
136 count -= 1
136 count -= 1
137 return path
137 return path
138
138
139 def fixmail(self, addr):
139 def fixmail(self, addr):
140 '''try to clean up email addresses.'''
140 '''try to clean up email addresses.'''
141
141
142 addr = util.email(addr.strip())
142 addr = util.email(addr.strip())
143 if self.domain:
143 if self.domain:
144 a = addr.find('@localhost')
144 a = addr.find('@localhost')
145 if a != -1:
145 if a != -1:
146 addr = addr[:a]
146 addr = addr[:a]
147 if '@' not in addr:
147 if '@' not in addr:
148 return addr + '@' + self.domain
148 return addr + '@' + self.domain
149 return addr
149 return addr
150
150
151 def subscribers(self):
151 def subscribers(self):
152 '''return list of email addresses of subscribers to this repo.'''
152 '''return list of email addresses of subscribers to this repo.'''
153 subs = set()
153 subs = set()
154 for user, pats in self.ui.configitems('usersubs'):
154 for user, pats in self.ui.configitems('usersubs'):
155 for pat in pats.split(','):
155 for pat in pats.split(','):
156 if fnmatch.fnmatch(self.repo.root, pat.strip()):
156 if fnmatch.fnmatch(self.repo.root, pat.strip()):
157 subs.add(self.fixmail(user))
157 subs.add(self.fixmail(user))
158 for pat, users in self.ui.configitems('reposubs'):
158 for pat, users in self.ui.configitems('reposubs'):
159 if fnmatch.fnmatch(self.repo.root, pat):
159 if fnmatch.fnmatch(self.repo.root, pat):
160 for user in users.split(','):
160 for user in users.split(','):
161 subs.add(self.fixmail(user))
161 subs.add(self.fixmail(user))
162 return [mail.addressencode(self.ui, s, self.charsets, self.test)
162 return [mail.addressencode(self.ui, s, self.charsets, self.test)
163 for s in sorted(subs)]
163 for s in sorted(subs)]
164
164
165 def url(self, path=None):
165 def url(self, path=None):
166 return self.ui.config('web', 'baseurl') + (path or self.root)
166 return self.ui.config('web', 'baseurl') + (path or self.root)
167
167
168 def node(self, ctx):
168 def node(self, ctx, **props):
169 '''format one changeset.'''
169 '''format one changeset.'''
170 self.t.show(ctx, changes=ctx.changeset(),
170 self.t.show(ctx, changes=ctx.changeset(),
171 baseurl=self.ui.config('web', 'baseurl'),
171 baseurl=self.ui.config('web', 'baseurl'),
172 root=self.repo.root, webroot=self.root)
172 root=self.repo.root, webroot=self.root, **props)
173
173
174 def skipsource(self, source):
174 def skipsource(self, source):
175 '''true if incoming changes from this source should be skipped.'''
175 '''true if incoming changes from this source should be skipped.'''
176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
177 return source not in ok_sources
177 return source not in ok_sources
178
178
179 def send(self, ctx, count, data):
179 def send(self, ctx, count, data):
180 '''send message.'''
180 '''send message.'''
181
181
182 p = email.Parser.Parser()
182 p = email.Parser.Parser()
183 try:
183 try:
184 msg = p.parsestr(data)
184 msg = p.parsestr(data)
185 except email.Errors.MessageParseError, inst:
185 except email.Errors.MessageParseError, inst:
186 raise util.Abort(inst)
186 raise util.Abort(inst)
187
187
188 # store sender and subject
188 # store sender and subject
189 sender, subject = msg['From'], msg['Subject']
189 sender, subject = msg['From'], msg['Subject']
190 del msg['From'], msg['Subject']
190 del msg['From'], msg['Subject']
191
191
192 if not msg.is_multipart():
192 if not msg.is_multipart():
193 # create fresh mime message from scratch
193 # create fresh mime message from scratch
194 # (multipart templates must take care of this themselves)
194 # (multipart templates must take care of this themselves)
195 headers = msg.items()
195 headers = msg.items()
196 payload = msg.get_payload()
196 payload = msg.get_payload()
197 # for notification prefer readability over data precision
197 # for notification prefer readability over data precision
198 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
198 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
199 # reinstate custom headers
199 # reinstate custom headers
200 for k, v in headers:
200 for k, v in headers:
201 msg[k] = v
201 msg[k] = v
202
202
203 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
203 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
204
204
205 # try to make subject line exist and be useful
205 # try to make subject line exist and be useful
206 if not subject:
206 if not subject:
207 if count > 1:
207 if count > 1:
208 subject = _('%s: %d new changesets') % (self.root, count)
208 subject = _('%s: %d new changesets') % (self.root, count)
209 else:
209 else:
210 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
210 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
211 subject = '%s: %s' % (self.root, s)
211 subject = '%s: %s' % (self.root, s)
212 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
212 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
213 if maxsubject and len(subject) > maxsubject:
213 if maxsubject and len(subject) > maxsubject:
214 subject = subject[:maxsubject-3] + '...'
214 subject = subject[:maxsubject-3] + '...'
215 msg['Subject'] = mail.headencode(self.ui, subject,
215 msg['Subject'] = mail.headencode(self.ui, subject,
216 self.charsets, self.test)
216 self.charsets, self.test)
217
217
218 # try to make message have proper sender
218 # try to make message have proper sender
219 if not sender:
219 if not sender:
220 sender = self.ui.config('email', 'from') or self.ui.username()
220 sender = self.ui.config('email', 'from') or self.ui.username()
221 if '@' not in sender or '@localhost' in sender:
221 if '@' not in sender or '@localhost' in sender:
222 sender = self.fixmail(sender)
222 sender = self.fixmail(sender)
223 msg['From'] = mail.addressencode(self.ui, sender,
223 msg['From'] = mail.addressencode(self.ui, sender,
224 self.charsets, self.test)
224 self.charsets, self.test)
225
225
226 msg['X-Hg-Notification'] = 'changeset %s' % ctx
226 msg['X-Hg-Notification'] = 'changeset %s' % ctx
227 if not msg['Message-Id']:
227 if not msg['Message-Id']:
228 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
228 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
229 (ctx, int(time.time()),
229 (ctx, int(time.time()),
230 hash(self.repo.root), socket.getfqdn()))
230 hash(self.repo.root), socket.getfqdn()))
231 msg['To'] = ', '.join(self.subs)
231 msg['To'] = ', '.join(self.subs)
232
232
233 msgtext = msg.as_string()
233 msgtext = msg.as_string()
234 if self.test:
234 if self.test:
235 self.ui.write(msgtext)
235 self.ui.write(msgtext)
236 if not msgtext.endswith('\n'):
236 if not msgtext.endswith('\n'):
237 self.ui.write('\n')
237 self.ui.write('\n')
238 else:
238 else:
239 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
239 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
240 (len(self.subs), count))
240 (len(self.subs), count))
241 mail.sendmail(self.ui, util.email(msg['From']),
241 mail.sendmail(self.ui, util.email(msg['From']),
242 self.subs, msgtext)
242 self.subs, msgtext)
243
243
244 def diff(self, ctx, ref=None):
244 def diff(self, ctx, ref=None):
245
245
246 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
246 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
247 prev = ctx.parents()[0].node()
247 prev = ctx.parents()[0].node()
248 ref = ref and ref.node() or ctx.node()
248 ref = ref and ref.node() or ctx.node()
249 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
249 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
250 difflines = ''.join(chunks).splitlines()
250 difflines = ''.join(chunks).splitlines()
251
251
252 if self.ui.configbool('notify', 'diffstat', True):
252 if self.ui.configbool('notify', 'diffstat', True):
253 s = patch.diffstat(difflines)
253 s = patch.diffstat(difflines)
254 # s may be nil, don't include the header if it is
254 # s may be nil, don't include the header if it is
255 if s:
255 if s:
256 self.ui.write('\ndiffstat:\n\n%s' % s)
256 self.ui.write('\ndiffstat:\n\n%s' % s)
257
257
258 if maxdiff == 0:
258 if maxdiff == 0:
259 return
259 return
260 elif maxdiff > 0 and len(difflines) > maxdiff:
260 elif maxdiff > 0 and len(difflines) > maxdiff:
261 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
261 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
262 self.ui.write(msg % (len(difflines), maxdiff))
262 self.ui.write(msg % (len(difflines), maxdiff))
263 difflines = difflines[:maxdiff]
263 difflines = difflines[:maxdiff]
264 elif difflines:
264 elif difflines:
265 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
265 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
266
266
267 self.ui.write("\n".join(difflines))
267 self.ui.write("\n".join(difflines))
268
268
269 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
269 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
270 '''send email notifications to interested subscribers.
270 '''send email notifications to interested subscribers.
271
271
272 if used as changegroup hook, send one email for all changesets in
272 if used as changegroup hook, send one email for all changesets in
273 changegroup. else send one email per changeset.'''
273 changegroup. else send one email per changeset.'''
274
274
275 n = notifier(ui, repo, hooktype)
275 n = notifier(ui, repo, hooktype)
276 ctx = repo[node]
276 ctx = repo[node]
277
277
278 if not n.subs:
278 if not n.subs:
279 ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
279 ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
280 return
280 return
281 if n.skipsource(source):
281 if n.skipsource(source):
282 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
282 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
283 return
283 return
284
284
285 ui.pushbuffer()
285 ui.pushbuffer()
286 if hooktype == 'changegroup':
286 if hooktype == 'changegroup':
287 start, end = ctx.rev(), len(repo)
287 start, end = ctx.rev(), len(repo)
288 count = end - start
288 count = end - start
289 for rev in xrange(start, end):
289 for rev in xrange(start, end):
290 n.node(repo[rev])
290 n.node(repo[rev])
291 n.diff(ctx, repo['tip'])
291 n.diff(ctx, repo['tip'])
292 else:
292 else:
293 count = 1
293 count = 1
294 n.node(ctx)
294 n.node(ctx)
295 n.diff(ctx)
295 n.diff(ctx)
296
296
297 data = ui.popbuffer()
297 data = ui.popbuffer()
298 n.send(ctx, count, data)
298 n.send(ctx, count, data)
General Comments 0
You need to be logged in to leave comments. Login now