##// END OF EJS Templates
notify: make a message translatable...
FUJIWARA Katsunori -
r29240:48afcaad default
parent child Browse files
Show More
@@ -1,430 +1,430 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 from __future__ import absolute_import
136 136
137 137 import email
138 138 import fnmatch
139 139 import socket
140 140 import time
141 141
142 142 from mercurial.i18n import _
143 143 from mercurial import (
144 144 cmdutil,
145 145 error,
146 146 mail,
147 147 patch,
148 148 util,
149 149 )
150 150
151 151 # Note for extension authors: ONLY specify testedwith = 'internal' for
152 152 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
153 153 # be specifying the version(s) of Mercurial they are tested with, or
154 154 # leave the attribute unspecified.
155 155 testedwith = 'internal'
156 156
157 157 # template for single changeset can include email headers.
158 158 single_template = '''
159 159 Subject: changeset in {webroot}: {desc|firstline|strip}
160 160 From: {author}
161 161
162 162 changeset {node|short} in {root}
163 163 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
164 164 description:
165 165 \t{desc|tabindent|strip}
166 166 '''.lstrip()
167 167
168 168 # template for multiple changesets should not contain email headers,
169 169 # because only first set of headers will be used and result will look
170 170 # strange.
171 171 multiple_template = '''
172 172 changeset {node|short} in {root}
173 173 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
174 174 summary: {desc|firstline}
175 175 '''
176 176
177 177 deftemplates = {
178 178 'changegroup': multiple_template,
179 179 }
180 180
181 181 class notifier(object):
182 182 '''email notification class.'''
183 183
184 184 def __init__(self, ui, repo, hooktype):
185 185 self.ui = ui
186 186 cfg = self.ui.config('notify', 'config')
187 187 if cfg:
188 188 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
189 189 self.repo = repo
190 190 self.stripcount = int(self.ui.config('notify', 'strip', 0))
191 191 self.root = self.strip(self.repo.root)
192 192 self.domain = self.ui.config('notify', 'domain')
193 193 self.mbox = self.ui.config('notify', 'mbox')
194 194 self.test = self.ui.configbool('notify', 'test', True)
195 195 self.charsets = mail._charsets(self.ui)
196 196 self.subs = self.subscribers()
197 197 self.merge = self.ui.configbool('notify', 'merge', True)
198 198
199 199 mapfile = None
200 200 template = (self.ui.config('notify', hooktype) or
201 201 self.ui.config('notify', 'template'))
202 202 if not template:
203 203 mapfile = self.ui.config('notify', 'style')
204 204 if not mapfile and not template:
205 205 template = deftemplates.get(hooktype) or single_template
206 206 self.t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
207 207 template, mapfile, False)
208 208
209 209 def strip(self, path):
210 210 '''strip leading slashes from local path, turn into web-safe path.'''
211 211
212 212 path = util.pconvert(path)
213 213 count = self.stripcount
214 214 while count > 0:
215 215 c = path.find('/')
216 216 if c == -1:
217 217 break
218 218 path = path[c + 1:]
219 219 count -= 1
220 220 return path
221 221
222 222 def fixmail(self, addr):
223 223 '''try to clean up email addresses.'''
224 224
225 225 addr = util.email(addr.strip())
226 226 if self.domain:
227 227 a = addr.find('@localhost')
228 228 if a != -1:
229 229 addr = addr[:a]
230 230 if '@' not in addr:
231 231 return addr + '@' + self.domain
232 232 return addr
233 233
234 234 def subscribers(self):
235 235 '''return list of email addresses of subscribers to this repo.'''
236 236 subs = set()
237 237 for user, pats in self.ui.configitems('usersubs'):
238 238 for pat in pats.split(','):
239 239 if '#' in pat:
240 240 pat, revs = pat.split('#', 1)
241 241 else:
242 242 revs = None
243 243 if fnmatch.fnmatch(self.repo.root, pat.strip()):
244 244 subs.add((self.fixmail(user), revs))
245 245 for pat, users in self.ui.configitems('reposubs'):
246 246 if '#' in pat:
247 247 pat, revs = pat.split('#', 1)
248 248 else:
249 249 revs = None
250 250 if fnmatch.fnmatch(self.repo.root, pat):
251 251 for user in users.split(','):
252 252 subs.add((self.fixmail(user), revs))
253 253 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
254 254 for s, r in sorted(subs)]
255 255
256 256 def node(self, ctx, **props):
257 257 '''format one changeset, unless it is a suppressed merge.'''
258 258 if not self.merge and len(ctx.parents()) > 1:
259 259 return False
260 260 self.t.show(ctx, changes=ctx.changeset(),
261 261 baseurl=self.ui.config('web', 'baseurl'),
262 262 root=self.repo.root, webroot=self.root, **props)
263 263 return True
264 264
265 265 def skipsource(self, source):
266 266 '''true if incoming changes from this source should be skipped.'''
267 267 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
268 268 return source not in ok_sources
269 269
270 270 def send(self, ctx, count, data):
271 271 '''send message.'''
272 272
273 273 # Select subscribers by revset
274 274 subs = set()
275 275 for sub, spec in self.subs:
276 276 if spec is None:
277 277 subs.add(sub)
278 278 continue
279 279 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
280 280 if len(revs):
281 281 subs.add(sub)
282 282 continue
283 283 if len(subs) == 0:
284 284 self.ui.debug('notify: no subscribers to selected repo '
285 285 'and revset\n')
286 286 return
287 287
288 288 p = email.Parser.Parser()
289 289 try:
290 290 msg = p.parsestr(data)
291 291 except email.Errors.MessageParseError as inst:
292 292 raise error.Abort(inst)
293 293
294 294 # store sender and subject
295 295 sender, subject = msg['From'], msg['Subject']
296 296 del msg['From'], msg['Subject']
297 297
298 298 if not msg.is_multipart():
299 299 # create fresh mime message from scratch
300 300 # (multipart templates must take care of this themselves)
301 301 headers = msg.items()
302 302 payload = msg.get_payload()
303 303 # for notification prefer readability over data precision
304 304 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
305 305 # reinstate custom headers
306 306 for k, v in headers:
307 307 msg[k] = v
308 308
309 309 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
310 310
311 311 # try to make subject line exist and be useful
312 312 if not subject:
313 313 if count > 1:
314 314 subject = _('%s: %d new changesets') % (self.root, count)
315 315 else:
316 316 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
317 317 subject = '%s: %s' % (self.root, s)
318 318 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
319 319 if maxsubject:
320 320 subject = util.ellipsis(subject, maxsubject)
321 321 msg['Subject'] = mail.headencode(self.ui, subject,
322 322 self.charsets, self.test)
323 323
324 324 # try to make message have proper sender
325 325 if not sender:
326 326 sender = self.ui.config('email', 'from') or self.ui.username()
327 327 if '@' not in sender or '@localhost' in sender:
328 328 sender = self.fixmail(sender)
329 329 msg['From'] = mail.addressencode(self.ui, sender,
330 330 self.charsets, self.test)
331 331
332 332 msg['X-Hg-Notification'] = 'changeset %s' % ctx
333 333 if not msg['Message-Id']:
334 334 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
335 335 (ctx, int(time.time()),
336 336 hash(self.repo.root), socket.getfqdn()))
337 337 msg['To'] = ', '.join(sorted(subs))
338 338
339 339 msgtext = msg.as_string()
340 340 if self.test:
341 341 self.ui.write(msgtext)
342 342 if not msgtext.endswith('\n'):
343 343 self.ui.write('\n')
344 344 else:
345 345 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
346 346 (len(subs), count))
347 347 mail.sendmail(self.ui, util.email(msg['From']),
348 348 subs, msgtext, mbox=self.mbox)
349 349
350 350 def diff(self, ctx, ref=None):
351 351
352 352 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
353 353 prev = ctx.p1().node()
354 354 if ref:
355 355 ref = ref.node()
356 356 else:
357 357 ref = ctx.node()
358 358 chunks = patch.diff(self.repo, prev, ref,
359 359 opts=patch.diffallopts(self.ui))
360 360 difflines = ''.join(chunks).splitlines()
361 361
362 362 if self.ui.configbool('notify', 'diffstat', True):
363 363 s = patch.diffstat(difflines)
364 364 # s may be nil, don't include the header if it is
365 365 if s:
366 self.ui.write('\ndiffstat:\n\n%s' % s)
366 self.ui.write(_('\ndiffstat:\n\n%s') % s)
367 367
368 368 if maxdiff == 0:
369 369 return
370 370 elif maxdiff > 0 and len(difflines) > maxdiff:
371 371 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
372 372 self.ui.write(msg % (len(difflines), maxdiff))
373 373 difflines = difflines[:maxdiff]
374 374 elif difflines:
375 375 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
376 376
377 377 self.ui.write("\n".join(difflines))
378 378
379 379 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
380 380 '''send email notifications to interested subscribers.
381 381
382 382 if used as changegroup hook, send one email for all changesets in
383 383 changegroup. else send one email per changeset.'''
384 384
385 385 n = notifier(ui, repo, hooktype)
386 386 ctx = repo[node]
387 387
388 388 if not n.subs:
389 389 ui.debug('notify: no subscribers to repository %s\n' % n.root)
390 390 return
391 391 if n.skipsource(source):
392 392 ui.debug('notify: changes have source "%s" - skipping\n' % source)
393 393 return
394 394
395 395 ui.pushbuffer()
396 396 data = ''
397 397 count = 0
398 398 author = ''
399 399 if hooktype == 'changegroup' or hooktype == 'outgoing':
400 400 start, end = ctx.rev(), len(repo)
401 401 for rev in xrange(start, end):
402 402 if n.node(repo[rev]):
403 403 count += 1
404 404 if not author:
405 405 author = repo[rev].user()
406 406 else:
407 407 data += ui.popbuffer()
408 408 ui.note(_('notify: suppressing notification for merge %d:%s\n')
409 409 % (rev, repo[rev].hex()[:12]))
410 410 ui.pushbuffer()
411 411 if count:
412 412 n.diff(ctx, repo['tip'])
413 413 else:
414 414 if not n.node(ctx):
415 415 ui.popbuffer()
416 416 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
417 417 (ctx.rev(), ctx.hex()[:12]))
418 418 return
419 419 count += 1
420 420 n.diff(ctx)
421 421 if not author:
422 422 author = ctx.user()
423 423
424 424 data += ui.popbuffer()
425 425 fromauthor = ui.config('notify', 'fromauthor')
426 426 if author and fromauthor:
427 427 data = '\n'.join(['From: %s' % author, data])
428 428
429 429 if count:
430 430 n.send(ctx, count, data)
General Comments 0
You need to be logged in to leave comments. Login now