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