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