##// 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 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 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 def node(self, ctx):
168 def node(self, ctx, **props):
169 169 '''format one changeset.'''
170 170 self.t.show(ctx, changes=ctx.changeset(),
171 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 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 183 try:
184 184 msg = p.parsestr(data)
185 185 except email.Errors.MessageParseError, inst:
186 186 raise util.Abort(inst)
187 187
188 188 # store sender and subject
189 189 sender, subject = msg['From'], msg['Subject']
190 190 del msg['From'], msg['Subject']
191 191
192 192 if not msg.is_multipart():
193 193 # create fresh mime message from scratch
194 194 # (multipart templates must take care of this themselves)
195 195 headers = msg.items()
196 196 payload = msg.get_payload()
197 197 # for notification prefer readability over data precision
198 198 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
199 199 # reinstate custom headers
200 200 for k, v in headers:
201 201 msg[k] = v
202 202
203 203 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
204 204
205 205 # try to make subject line exist and be useful
206 206 if not subject:
207 207 if count > 1:
208 208 subject = _('%s: %d new changesets') % (self.root, count)
209 209 else:
210 210 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
211 211 subject = '%s: %s' % (self.root, s)
212 212 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
213 213 if maxsubject and len(subject) > maxsubject:
214 214 subject = subject[:maxsubject-3] + '...'
215 215 msg['Subject'] = mail.headencode(self.ui, subject,
216 216 self.charsets, self.test)
217 217
218 218 # try to make message have proper sender
219 219 if not sender:
220 220 sender = self.ui.config('email', 'from') or self.ui.username()
221 221 if '@' not in sender or '@localhost' in sender:
222 222 sender = self.fixmail(sender)
223 223 msg['From'] = mail.addressencode(self.ui, sender,
224 224 self.charsets, self.test)
225 225
226 226 msg['X-Hg-Notification'] = 'changeset %s' % ctx
227 227 if not msg['Message-Id']:
228 228 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
229 229 (ctx, int(time.time()),
230 230 hash(self.repo.root), socket.getfqdn()))
231 231 msg['To'] = ', '.join(self.subs)
232 232
233 233 msgtext = msg.as_string()
234 234 if self.test:
235 235 self.ui.write(msgtext)
236 236 if not msgtext.endswith('\n'):
237 237 self.ui.write('\n')
238 238 else:
239 239 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
240 240 (len(self.subs), count))
241 241 mail.sendmail(self.ui, util.email(msg['From']),
242 242 self.subs, msgtext)
243 243
244 244 def diff(self, ctx, ref=None):
245 245
246 246 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
247 247 prev = ctx.parents()[0].node()
248 248 ref = ref and ref.node() or ctx.node()
249 249 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
250 250 difflines = ''.join(chunks).splitlines()
251 251
252 252 if self.ui.configbool('notify', 'diffstat', True):
253 253 s = patch.diffstat(difflines)
254 254 # s may be nil, don't include the header if it is
255 255 if s:
256 256 self.ui.write('\ndiffstat:\n\n%s' % s)
257 257
258 258 if maxdiff == 0:
259 259 return
260 260 elif maxdiff > 0 and len(difflines) > maxdiff:
261 261 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
262 262 self.ui.write(msg % (len(difflines), maxdiff))
263 263 difflines = difflines[:maxdiff]
264 264 elif difflines:
265 265 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
266 266
267 267 self.ui.write("\n".join(difflines))
268 268
269 269 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
270 270 '''send email notifications to interested subscribers.
271 271
272 272 if used as changegroup hook, send one email for all changesets in
273 273 changegroup. else send one email per changeset.'''
274 274
275 275 n = notifier(ui, repo, hooktype)
276 276 ctx = repo[node]
277 277
278 278 if not n.subs:
279 279 ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
280 280 return
281 281 if n.skipsource(source):
282 282 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
283 283 return
284 284
285 285 ui.pushbuffer()
286 286 if hooktype == 'changegroup':
287 287 start, end = ctx.rev(), len(repo)
288 288 count = end - start
289 289 for rev in xrange(start, end):
290 290 n.node(repo[rev])
291 291 n.diff(ctx, repo['tip'])
292 292 else:
293 293 count = 1
294 294 n.node(ctx)
295 295 n.diff(ctx)
296 296
297 297 data = ui.popbuffer()
298 298 n.send(ctx, count, data)
General Comments 0
You need to be logged in to leave comments. Login now