##// END OF EJS Templates
notify: drop import required by Python 2.4...
Pierre-Yves David -
r25203:d4ccc455 default
parent child Browse files
Show More
@@ -1,419 +1,416 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 or any later version.
7 7
8 8 '''hooks for sending email push notifications
9 9
10 10 This extension implements hooks to send email notifications when
11 11 changesets are sent from or received by the local repository.
12 12
13 13 First, enable the extension as explained in :hg:`help extensions`, and
14 14 register the hook you want to run. ``incoming`` and ``changegroup`` hooks
15 15 are run when changesets are received, while ``outgoing`` hooks are for
16 16 changesets sent to another repository::
17 17
18 18 [hooks]
19 19 # one email for each incoming changeset
20 20 incoming.notify = python:hgext.notify.hook
21 21 # one email for all incoming changesets
22 22 changegroup.notify = python:hgext.notify.hook
23 23
24 24 # one email for all outgoing changesets
25 25 outgoing.notify = python:hgext.notify.hook
26 26
27 27 This registers the hooks. To enable notification, subscribers must
28 28 be assigned to repositories. The ``[usersubs]`` section maps multiple
29 29 repositories to a given recipient. The ``[reposubs]`` section maps
30 30 multiple recipients to a single repository::
31 31
32 32 [usersubs]
33 33 # key is subscriber email, value is a comma-separated list of repo patterns
34 34 user@host = pattern
35 35
36 36 [reposubs]
37 37 # key is repo pattern, value is a comma-separated list of subscriber emails
38 38 pattern = user@host
39 39
40 40 A ``pattern`` is a ``glob`` matching the absolute path to a repository,
41 41 optionally combined with a revset expression. A revset expression, if
42 42 present, is separated from the glob by a hash. Example::
43 43
44 44 [reposubs]
45 45 */widgets#branch(release) = qa-team@example.com
46 46
47 47 This sends to ``qa-team@example.com`` whenever a changeset on the ``release``
48 48 branch triggers a notification in any repository ending in ``widgets``.
49 49
50 50 In order to place them under direct user management, ``[usersubs]`` and
51 51 ``[reposubs]`` sections may be placed in a separate ``hgrc`` file and
52 52 incorporated by reference::
53 53
54 54 [notify]
55 55 config = /path/to/subscriptionsfile
56 56
57 57 Notifications will not be sent until the ``notify.test`` value is set
58 58 to ``False``; see below.
59 59
60 60 Notifications content can be tweaked with the following configuration entries:
61 61
62 62 notify.test
63 63 If ``True``, print messages to stdout instead of sending them. Default: True.
64 64
65 65 notify.sources
66 66 Space-separated list of change sources. Notifications are activated only
67 67 when a changeset's source is in this list. Sources may be:
68 68
69 69 :``serve``: changesets received via http or ssh
70 70 :``pull``: changesets received via ``hg pull``
71 71 :``unbundle``: changesets received via ``hg unbundle``
72 72 :``push``: changesets sent or received via ``hg push``
73 73 :``bundle``: changesets sent via ``hg unbundle``
74 74
75 75 Default: serve.
76 76
77 77 notify.strip
78 78 Number of leading slashes to strip from url paths. By default, notifications
79 79 reference repositories with their absolute path. ``notify.strip`` lets you
80 80 turn them into relative paths. For example, ``notify.strip=3`` will change
81 81 ``/long/path/repository`` into ``repository``. Default: 0.
82 82
83 83 notify.domain
84 84 Default email domain for sender or recipients with no explicit domain.
85 85
86 86 notify.style
87 87 Style file to use when formatting emails.
88 88
89 89 notify.template
90 90 Template to use when formatting emails.
91 91
92 92 notify.incoming
93 93 Template to use when run as an incoming hook, overriding ``notify.template``.
94 94
95 95 notify.outgoing
96 96 Template to use when run as an outgoing hook, overriding ``notify.template``.
97 97
98 98 notify.changegroup
99 99 Template to use when running as a changegroup hook, overriding
100 100 ``notify.template``.
101 101
102 102 notify.maxdiff
103 103 Maximum number of diff lines to include in notification email. Set to 0
104 104 to disable the diff, or -1 to include all of it. Default: 300.
105 105
106 106 notify.maxsubject
107 107 Maximum number of characters in email's subject line. Default: 67.
108 108
109 109 notify.diffstat
110 110 Set to True to include a diffstat before diff content. Default: True.
111 111
112 112 notify.merge
113 113 If True, send notifications for merge changesets. Default: True.
114 114
115 115 notify.mbox
116 116 If set, append mails to this mbox file instead of sending. Default: None.
117 117
118 118 notify.fromauthor
119 119 If set, use the committer of the first changeset in a changegroup for
120 120 the "From" field of the notification mail. If not set, take the user
121 121 from the pushing repo. Default: False.
122 122
123 123 If set, the following entries will also be used to customize the
124 124 notifications:
125 125
126 126 email.from
127 127 Email ``From`` address to use if none can be found in the generated
128 128 email content.
129 129
130 130 web.baseurl
131 131 Root repository URL to combine with repository paths when making
132 132 references. See also ``notify.strip``.
133 133
134 134 '''
135 135
136 136 import email, socket, time
137 # On python2.4 you have to import this by name or they fail to
138 # load. This was not a problem on Python 2.7.
139 import email.Parser
140 137 from mercurial.i18n import _
141 138 from mercurial import patch, cmdutil, util, mail
142 139 import fnmatch
143 140
144 141 # Note for extension authors: ONLY specify testedwith = 'internal' for
145 142 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
146 143 # be specifying the version(s) of Mercurial they are tested with, or
147 144 # leave the attribute unspecified.
148 145 testedwith = 'internal'
149 146
150 147 # template for single changeset can include email headers.
151 148 single_template = '''
152 149 Subject: changeset in {webroot}: {desc|firstline|strip}
153 150 From: {author}
154 151
155 152 changeset {node|short} in {root}
156 153 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
157 154 description:
158 155 \t{desc|tabindent|strip}
159 156 '''.lstrip()
160 157
161 158 # template for multiple changesets should not contain email headers,
162 159 # because only first set of headers will be used and result will look
163 160 # strange.
164 161 multiple_template = '''
165 162 changeset {node|short} in {root}
166 163 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
167 164 summary: {desc|firstline}
168 165 '''
169 166
170 167 deftemplates = {
171 168 'changegroup': multiple_template,
172 169 }
173 170
174 171 class notifier(object):
175 172 '''email notification class.'''
176 173
177 174 def __init__(self, ui, repo, hooktype):
178 175 self.ui = ui
179 176 cfg = self.ui.config('notify', 'config')
180 177 if cfg:
181 178 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
182 179 self.repo = repo
183 180 self.stripcount = int(self.ui.config('notify', 'strip', 0))
184 181 self.root = self.strip(self.repo.root)
185 182 self.domain = self.ui.config('notify', 'domain')
186 183 self.mbox = self.ui.config('notify', 'mbox')
187 184 self.test = self.ui.configbool('notify', 'test', True)
188 185 self.charsets = mail._charsets(self.ui)
189 186 self.subs = self.subscribers()
190 187 self.merge = self.ui.configbool('notify', 'merge', True)
191 188
192 189 mapfile = self.ui.config('notify', 'style')
193 190 template = (self.ui.config('notify', hooktype) or
194 191 self.ui.config('notify', 'template'))
195 192 if not mapfile and not template:
196 193 template = deftemplates.get(hooktype) or single_template
197 194 self.t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
198 195 template, mapfile, False)
199 196
200 197 def strip(self, path):
201 198 '''strip leading slashes from local path, turn into web-safe path.'''
202 199
203 200 path = util.pconvert(path)
204 201 count = self.stripcount
205 202 while count > 0:
206 203 c = path.find('/')
207 204 if c == -1:
208 205 break
209 206 path = path[c + 1:]
210 207 count -= 1
211 208 return path
212 209
213 210 def fixmail(self, addr):
214 211 '''try to clean up email addresses.'''
215 212
216 213 addr = util.email(addr.strip())
217 214 if self.domain:
218 215 a = addr.find('@localhost')
219 216 if a != -1:
220 217 addr = addr[:a]
221 218 if '@' not in addr:
222 219 return addr + '@' + self.domain
223 220 return addr
224 221
225 222 def subscribers(self):
226 223 '''return list of email addresses of subscribers to this repo.'''
227 224 subs = set()
228 225 for user, pats in self.ui.configitems('usersubs'):
229 226 for pat in pats.split(','):
230 227 if '#' in pat:
231 228 pat, revs = pat.split('#', 1)
232 229 else:
233 230 revs = None
234 231 if fnmatch.fnmatch(self.repo.root, pat.strip()):
235 232 subs.add((self.fixmail(user), revs))
236 233 for pat, users in self.ui.configitems('reposubs'):
237 234 if '#' in pat:
238 235 pat, revs = pat.split('#', 1)
239 236 else:
240 237 revs = None
241 238 if fnmatch.fnmatch(self.repo.root, pat):
242 239 for user in users.split(','):
243 240 subs.add((self.fixmail(user), revs))
244 241 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
245 242 for s, r in sorted(subs)]
246 243
247 244 def node(self, ctx, **props):
248 245 '''format one changeset, unless it is a suppressed merge.'''
249 246 if not self.merge and len(ctx.parents()) > 1:
250 247 return False
251 248 self.t.show(ctx, changes=ctx.changeset(),
252 249 baseurl=self.ui.config('web', 'baseurl'),
253 250 root=self.repo.root, webroot=self.root, **props)
254 251 return True
255 252
256 253 def skipsource(self, source):
257 254 '''true if incoming changes from this source should be skipped.'''
258 255 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
259 256 return source not in ok_sources
260 257
261 258 def send(self, ctx, count, data):
262 259 '''send message.'''
263 260
264 261 # Select subscribers by revset
265 262 subs = set()
266 263 for sub, spec in self.subs:
267 264 if spec is None:
268 265 subs.add(sub)
269 266 continue
270 267 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
271 268 if len(revs):
272 269 subs.add(sub)
273 270 continue
274 271 if len(subs) == 0:
275 272 self.ui.debug('notify: no subscribers to selected repo '
276 273 'and revset\n')
277 274 return
278 275
279 276 p = email.Parser.Parser()
280 277 try:
281 278 msg = p.parsestr(data)
282 279 except email.Errors.MessageParseError, inst:
283 280 raise util.Abort(inst)
284 281
285 282 # store sender and subject
286 283 sender, subject = msg['From'], msg['Subject']
287 284 del msg['From'], msg['Subject']
288 285
289 286 if not msg.is_multipart():
290 287 # create fresh mime message from scratch
291 288 # (multipart templates must take care of this themselves)
292 289 headers = msg.items()
293 290 payload = msg.get_payload()
294 291 # for notification prefer readability over data precision
295 292 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
296 293 # reinstate custom headers
297 294 for k, v in headers:
298 295 msg[k] = v
299 296
300 297 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
301 298
302 299 # try to make subject line exist and be useful
303 300 if not subject:
304 301 if count > 1:
305 302 subject = _('%s: %d new changesets') % (self.root, count)
306 303 else:
307 304 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
308 305 subject = '%s: %s' % (self.root, s)
309 306 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
310 307 if maxsubject:
311 308 subject = util.ellipsis(subject, maxsubject)
312 309 msg['Subject'] = mail.headencode(self.ui, subject,
313 310 self.charsets, self.test)
314 311
315 312 # try to make message have proper sender
316 313 if not sender:
317 314 sender = self.ui.config('email', 'from') or self.ui.username()
318 315 if '@' not in sender or '@localhost' in sender:
319 316 sender = self.fixmail(sender)
320 317 msg['From'] = mail.addressencode(self.ui, sender,
321 318 self.charsets, self.test)
322 319
323 320 msg['X-Hg-Notification'] = 'changeset %s' % ctx
324 321 if not msg['Message-Id']:
325 322 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
326 323 (ctx, int(time.time()),
327 324 hash(self.repo.root), socket.getfqdn()))
328 325 msg['To'] = ', '.join(sorted(subs))
329 326
330 327 msgtext = msg.as_string()
331 328 if self.test:
332 329 self.ui.write(msgtext)
333 330 if not msgtext.endswith('\n'):
334 331 self.ui.write('\n')
335 332 else:
336 333 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
337 334 (len(subs), count))
338 335 mail.sendmail(self.ui, util.email(msg['From']),
339 336 subs, msgtext, mbox=self.mbox)
340 337
341 338 def diff(self, ctx, ref=None):
342 339
343 340 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
344 341 prev = ctx.p1().node()
345 342 if ref:
346 343 ref = ref.node()
347 344 else:
348 345 ref = ctx.node()
349 346 chunks = patch.diff(self.repo, prev, ref,
350 347 opts=patch.diffallopts(self.ui))
351 348 difflines = ''.join(chunks).splitlines()
352 349
353 350 if self.ui.configbool('notify', 'diffstat', True):
354 351 s = patch.diffstat(difflines)
355 352 # s may be nil, don't include the header if it is
356 353 if s:
357 354 self.ui.write('\ndiffstat:\n\n%s' % s)
358 355
359 356 if maxdiff == 0:
360 357 return
361 358 elif maxdiff > 0 and len(difflines) > maxdiff:
362 359 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
363 360 self.ui.write(msg % (len(difflines), maxdiff))
364 361 difflines = difflines[:maxdiff]
365 362 elif difflines:
366 363 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
367 364
368 365 self.ui.write("\n".join(difflines))
369 366
370 367 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
371 368 '''send email notifications to interested subscribers.
372 369
373 370 if used as changegroup hook, send one email for all changesets in
374 371 changegroup. else send one email per changeset.'''
375 372
376 373 n = notifier(ui, repo, hooktype)
377 374 ctx = repo[node]
378 375
379 376 if not n.subs:
380 377 ui.debug('notify: no subscribers to repository %s\n' % n.root)
381 378 return
382 379 if n.skipsource(source):
383 380 ui.debug('notify: changes have source "%s" - skipping\n' % source)
384 381 return
385 382
386 383 ui.pushbuffer()
387 384 data = ''
388 385 count = 0
389 386 author = ''
390 387 if hooktype == 'changegroup' or hooktype == 'outgoing':
391 388 start, end = ctx.rev(), len(repo)
392 389 for rev in xrange(start, end):
393 390 if n.node(repo[rev]):
394 391 count += 1
395 392 if not author:
396 393 author = repo[rev].user()
397 394 else:
398 395 data += ui.popbuffer()
399 396 ui.note(_('notify: suppressing notification for merge %d:%s\n')
400 397 % (rev, repo[rev].hex()[:12]))
401 398 ui.pushbuffer()
402 399 if count:
403 400 n.diff(ctx, repo['tip'])
404 401 else:
405 402 if not n.node(ctx):
406 403 ui.popbuffer()
407 404 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
408 405 (ctx.rev(), ctx.hex()[:12]))
409 406 return
410 407 count += 1
411 408 n.diff(ctx)
412 409
413 410 data += ui.popbuffer()
414 411 fromauthor = ui.config('notify', 'fromauthor')
415 412 if author and fromauthor:
416 413 data = '\n'.join(['From: %s' % author, data])
417 414
418 415 if count:
419 416 n.send(ctx, count, data)
General Comments 0
You need to be logged in to leave comments. Login now