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