171 |
171 |
172 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
172 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
173 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
173 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
174 # be specifying the version(s) of Mercurial they are tested with, or |
174 # be specifying the version(s) of Mercurial they are tested with, or |
175 # leave the attribute unspecified. |
175 # leave the attribute unspecified. |
176 testedwith = 'ships-with-hg-core' |
176 testedwith = b'ships-with-hg-core' |
177 |
177 |
178 configtable = {} |
178 configtable = {} |
179 configitem = registrar.configitem(configtable) |
179 configitem = registrar.configitem(configtable) |
180 |
180 |
181 configitem( |
181 configitem( |
182 'notify', 'changegroup', default=None, |
182 b'notify', b'changegroup', default=None, |
183 ) |
183 ) |
184 configitem( |
184 configitem( |
185 'notify', 'config', default=None, |
185 b'notify', b'config', default=None, |
186 ) |
186 ) |
187 configitem( |
187 configitem( |
188 'notify', 'diffstat', default=True, |
188 b'notify', b'diffstat', default=True, |
189 ) |
189 ) |
190 configitem( |
190 configitem( |
191 'notify', 'domain', default=None, |
191 b'notify', b'domain', default=None, |
192 ) |
192 ) |
193 configitem( |
193 configitem( |
194 'notify', 'messageidseed', default=None, |
194 b'notify', b'messageidseed', default=None, |
195 ) |
195 ) |
196 configitem( |
196 configitem( |
197 'notify', 'fromauthor', default=None, |
197 b'notify', b'fromauthor', default=None, |
198 ) |
198 ) |
199 configitem( |
199 configitem( |
200 'notify', 'incoming', default=None, |
200 b'notify', b'incoming', default=None, |
201 ) |
201 ) |
202 configitem( |
202 configitem( |
203 'notify', 'maxdiff', default=300, |
203 b'notify', b'maxdiff', default=300, |
204 ) |
204 ) |
205 configitem( |
205 configitem( |
206 'notify', 'maxdiffstat', default=-1, |
206 b'notify', b'maxdiffstat', default=-1, |
207 ) |
207 ) |
208 configitem( |
208 configitem( |
209 'notify', 'maxsubject', default=67, |
209 b'notify', b'maxsubject', default=67, |
210 ) |
210 ) |
211 configitem( |
211 configitem( |
212 'notify', 'mbox', default=None, |
212 b'notify', b'mbox', default=None, |
213 ) |
213 ) |
214 configitem( |
214 configitem( |
215 'notify', 'merge', default=True, |
215 b'notify', b'merge', default=True, |
216 ) |
216 ) |
217 configitem( |
217 configitem( |
218 'notify', 'outgoing', default=None, |
218 b'notify', b'outgoing', default=None, |
219 ) |
219 ) |
220 configitem( |
220 configitem( |
221 'notify', 'sources', default='serve', |
221 b'notify', b'sources', default=b'serve', |
222 ) |
222 ) |
223 configitem( |
223 configitem( |
224 'notify', 'showfunc', default=None, |
224 b'notify', b'showfunc', default=None, |
225 ) |
225 ) |
226 configitem( |
226 configitem( |
227 'notify', 'strip', default=0, |
227 b'notify', b'strip', default=0, |
228 ) |
228 ) |
229 configitem( |
229 configitem( |
230 'notify', 'style', default=None, |
230 b'notify', b'style', default=None, |
231 ) |
231 ) |
232 configitem( |
232 configitem( |
233 'notify', 'template', default=None, |
233 b'notify', b'template', default=None, |
234 ) |
234 ) |
235 configitem( |
235 configitem( |
236 'notify', 'test', default=True, |
236 b'notify', b'test', default=True, |
237 ) |
237 ) |
238 |
238 |
239 # template for single changeset can include email headers. |
239 # template for single changeset can include email headers. |
240 single_template = b''' |
240 single_template = b''' |
241 Subject: changeset in {webroot}: {desc|firstline|strip} |
241 Subject: changeset in {webroot}: {desc|firstline|strip} |
255 details: {baseurl}{webroot}?cmd=changeset;node={node|short} |
255 details: {baseurl}{webroot}?cmd=changeset;node={node|short} |
256 summary: {desc|firstline} |
256 summary: {desc|firstline} |
257 ''' |
257 ''' |
258 |
258 |
259 deftemplates = { |
259 deftemplates = { |
260 'changegroup': multiple_template, |
260 b'changegroup': multiple_template, |
261 } |
261 } |
262 |
262 |
263 |
263 |
264 class notifier(object): |
264 class notifier(object): |
265 '''email notification class.''' |
265 '''email notification class.''' |
266 |
266 |
267 def __init__(self, ui, repo, hooktype): |
267 def __init__(self, ui, repo, hooktype): |
268 self.ui = ui |
268 self.ui = ui |
269 cfg = self.ui.config('notify', 'config') |
269 cfg = self.ui.config(b'notify', b'config') |
270 if cfg: |
270 if cfg: |
271 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs']) |
271 self.ui.readconfig(cfg, sections=[b'usersubs', b'reposubs']) |
272 self.repo = repo |
272 self.repo = repo |
273 self.stripcount = int(self.ui.config('notify', 'strip')) |
273 self.stripcount = int(self.ui.config(b'notify', b'strip')) |
274 self.root = self.strip(self.repo.root) |
274 self.root = self.strip(self.repo.root) |
275 self.domain = self.ui.config('notify', 'domain') |
275 self.domain = self.ui.config(b'notify', b'domain') |
276 self.mbox = self.ui.config('notify', 'mbox') |
276 self.mbox = self.ui.config(b'notify', b'mbox') |
277 self.test = self.ui.configbool('notify', 'test') |
277 self.test = self.ui.configbool(b'notify', b'test') |
278 self.charsets = mail._charsets(self.ui) |
278 self.charsets = mail._charsets(self.ui) |
279 self.subs = self.subscribers() |
279 self.subs = self.subscribers() |
280 self.merge = self.ui.configbool('notify', 'merge') |
280 self.merge = self.ui.configbool(b'notify', b'merge') |
281 self.showfunc = self.ui.configbool('notify', 'showfunc') |
281 self.showfunc = self.ui.configbool(b'notify', b'showfunc') |
282 self.messageidseed = self.ui.config('notify', 'messageidseed') |
282 self.messageidseed = self.ui.config(b'notify', b'messageidseed') |
283 if self.showfunc is None: |
283 if self.showfunc is None: |
284 self.showfunc = self.ui.configbool('diff', 'showfunc') |
284 self.showfunc = self.ui.configbool(b'diff', b'showfunc') |
285 |
285 |
286 mapfile = None |
286 mapfile = None |
287 template = self.ui.config('notify', hooktype) or self.ui.config( |
287 template = self.ui.config(b'notify', hooktype) or self.ui.config( |
288 'notify', 'template' |
288 b'notify', b'template' |
289 ) |
289 ) |
290 if not template: |
290 if not template: |
291 mapfile = self.ui.config('notify', 'style') |
291 mapfile = self.ui.config(b'notify', b'style') |
292 if not mapfile and not template: |
292 if not mapfile and not template: |
293 template = deftemplates.get(hooktype) or single_template |
293 template = deftemplates.get(hooktype) or single_template |
294 spec = logcmdutil.templatespec(template, mapfile) |
294 spec = logcmdutil.templatespec(template, mapfile) |
295 self.t = logcmdutil.changesettemplater(self.ui, self.repo, spec) |
295 self.t = logcmdutil.changesettemplater(self.ui, self.repo, spec) |
296 |
296 |
310 def fixmail(self, addr): |
310 def fixmail(self, addr): |
311 '''try to clean up email addresses.''' |
311 '''try to clean up email addresses.''' |
312 |
312 |
313 addr = stringutil.email(addr.strip()) |
313 addr = stringutil.email(addr.strip()) |
314 if self.domain: |
314 if self.domain: |
315 a = addr.find('@localhost') |
315 a = addr.find(b'@localhost') |
316 if a != -1: |
316 if a != -1: |
317 addr = addr[:a] |
317 addr = addr[:a] |
318 if '@' not in addr: |
318 if b'@' not in addr: |
319 return addr + '@' + self.domain |
319 return addr + b'@' + self.domain |
320 return addr |
320 return addr |
321 |
321 |
322 def subscribers(self): |
322 def subscribers(self): |
323 '''return list of email addresses of subscribers to this repo.''' |
323 '''return list of email addresses of subscribers to this repo.''' |
324 subs = set() |
324 subs = set() |
325 for user, pats in self.ui.configitems('usersubs'): |
325 for user, pats in self.ui.configitems(b'usersubs'): |
326 for pat in pats.split(','): |
326 for pat in pats.split(b','): |
327 if '#' in pat: |
327 if b'#' in pat: |
328 pat, revs = pat.split('#', 1) |
328 pat, revs = pat.split(b'#', 1) |
329 else: |
329 else: |
330 revs = None |
330 revs = None |
331 if fnmatch.fnmatch(self.repo.root, pat.strip()): |
331 if fnmatch.fnmatch(self.repo.root, pat.strip()): |
332 subs.add((self.fixmail(user), revs)) |
332 subs.add((self.fixmail(user), revs)) |
333 for pat, users in self.ui.configitems('reposubs'): |
333 for pat, users in self.ui.configitems(b'reposubs'): |
334 if '#' in pat: |
334 if b'#' in pat: |
335 pat, revs = pat.split('#', 1) |
335 pat, revs = pat.split(b'#', 1) |
336 else: |
336 else: |
337 revs = None |
337 revs = None |
338 if fnmatch.fnmatch(self.repo.root, pat): |
338 if fnmatch.fnmatch(self.repo.root, pat): |
339 for user in users.split(','): |
339 for user in users.split(b','): |
340 subs.add((self.fixmail(user), revs)) |
340 subs.add((self.fixmail(user), revs)) |
341 return [ |
341 return [ |
342 (mail.addressencode(self.ui, s, self.charsets, self.test), r) |
342 (mail.addressencode(self.ui, s, self.charsets, self.test), r) |
343 for s, r in sorted(subs) |
343 for s, r in sorted(subs) |
344 ] |
344 ] |
348 if not self.merge and len(ctx.parents()) > 1: |
348 if not self.merge and len(ctx.parents()) > 1: |
349 return False |
349 return False |
350 self.t.show( |
350 self.t.show( |
351 ctx, |
351 ctx, |
352 changes=ctx.changeset(), |
352 changes=ctx.changeset(), |
353 baseurl=self.ui.config('web', 'baseurl'), |
353 baseurl=self.ui.config(b'web', b'baseurl'), |
354 root=self.repo.root, |
354 root=self.repo.root, |
355 webroot=self.root, |
355 webroot=self.root, |
356 **props |
356 **props |
357 ) |
357 ) |
358 return True |
358 return True |
359 |
359 |
360 def skipsource(self, source): |
360 def skipsource(self, source): |
361 '''true if incoming changes from this source should be skipped.''' |
361 '''true if incoming changes from this source should be skipped.''' |
362 ok_sources = self.ui.config('notify', 'sources').split() |
362 ok_sources = self.ui.config(b'notify', b'sources').split() |
363 return source not in ok_sources |
363 return source not in ok_sources |
364 |
364 |
365 def send(self, ctx, count, data): |
365 def send(self, ctx, count, data): |
366 '''send message.''' |
366 '''send message.''' |
367 |
367 |
406 # reinstate custom headers |
406 # reinstate custom headers |
407 for k, v in headers: |
407 for k, v in headers: |
408 msg[k] = v |
408 msg[k] = v |
409 |
409 |
410 msg[r'Date'] = encoding.strfromlocal( |
410 msg[r'Date'] = encoding.strfromlocal( |
411 dateutil.datestr(format="%a, %d %b %Y %H:%M:%S %1%2") |
411 dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2") |
412 ) |
412 ) |
413 |
413 |
414 # try to make subject line exist and be useful |
414 # try to make subject line exist and be useful |
415 if not subject: |
415 if not subject: |
416 if count > 1: |
416 if count > 1: |
417 subject = _('%s: %d new changesets') % (self.root, count) |
417 subject = _(b'%s: %d new changesets') % (self.root, count) |
418 else: |
418 else: |
419 s = ctx.description().lstrip().split('\n', 1)[0].rstrip() |
419 s = ctx.description().lstrip().split(b'\n', 1)[0].rstrip() |
420 subject = '%s: %s' % (self.root, s) |
420 subject = b'%s: %s' % (self.root, s) |
421 maxsubject = int(self.ui.config('notify', 'maxsubject')) |
421 maxsubject = int(self.ui.config(b'notify', b'maxsubject')) |
422 if maxsubject: |
422 if maxsubject: |
423 subject = stringutil.ellipsis(subject, maxsubject) |
423 subject = stringutil.ellipsis(subject, maxsubject) |
424 msg[r'Subject'] = encoding.strfromlocal( |
424 msg[r'Subject'] = encoding.strfromlocal( |
425 mail.headencode(self.ui, subject, self.charsets, self.test) |
425 mail.headencode(self.ui, subject, self.charsets, self.test) |
426 ) |
426 ) |
427 |
427 |
428 # try to make message have proper sender |
428 # try to make message have proper sender |
429 if not sender: |
429 if not sender: |
430 sender = self.ui.config('email', 'from') or self.ui.username() |
430 sender = self.ui.config(b'email', b'from') or self.ui.username() |
431 if '@' not in sender or '@localhost' in sender: |
431 if b'@' not in sender or b'@localhost' in sender: |
432 sender = self.fixmail(sender) |
432 sender = self.fixmail(sender) |
433 msg[r'From'] = encoding.strfromlocal( |
433 msg[r'From'] = encoding.strfromlocal( |
434 mail.addressencode(self.ui, sender, self.charsets, self.test) |
434 mail.addressencode(self.ui, sender, self.charsets, self.test) |
435 ) |
435 ) |
436 |
436 |
437 msg[r'X-Hg-Notification'] = r'changeset %s' % ctx |
437 msg[r'X-Hg-Notification'] = r'changeset %s' % ctx |
438 if not msg[r'Message-Id']: |
438 if not msg[r'Message-Id']: |
439 msg[r'Message-Id'] = messageid(ctx, self.domain, self.messageidseed) |
439 msg[r'Message-Id'] = messageid(ctx, self.domain, self.messageidseed) |
440 msg[r'To'] = encoding.strfromlocal(', '.join(sorted(subs))) |
440 msg[r'To'] = encoding.strfromlocal(b', '.join(sorted(subs))) |
441 |
441 |
442 msgtext = encoding.strtolocal(msg.as_string()) |
442 msgtext = encoding.strtolocal(msg.as_string()) |
443 if self.test: |
443 if self.test: |
444 self.ui.write(msgtext) |
444 self.ui.write(msgtext) |
445 if not msgtext.endswith('\n'): |
445 if not msgtext.endswith(b'\n'): |
446 self.ui.write('\n') |
446 self.ui.write(b'\n') |
447 else: |
447 else: |
448 self.ui.status( |
448 self.ui.status( |
449 _('notify: sending %d subscribers %d changes\n') |
449 _(b'notify: sending %d subscribers %d changes\n') |
450 % (len(subs), count) |
450 % (len(subs), count) |
451 ) |
451 ) |
452 mail.sendmail( |
452 mail.sendmail( |
453 self.ui, |
453 self.ui, |
454 stringutil.email(msg[r'From']), |
454 stringutil.email(msg[r'From']), |
457 mbox=self.mbox, |
457 mbox=self.mbox, |
458 ) |
458 ) |
459 |
459 |
460 def diff(self, ctx, ref=None): |
460 def diff(self, ctx, ref=None): |
461 |
461 |
462 maxdiff = int(self.ui.config('notify', 'maxdiff')) |
462 maxdiff = int(self.ui.config(b'notify', b'maxdiff')) |
463 prev = ctx.p1().node() |
463 prev = ctx.p1().node() |
464 if ref: |
464 if ref: |
465 ref = ref.node() |
465 ref = ref.node() |
466 else: |
466 else: |
467 ref = ctx.node() |
467 ref = ctx.node() |
468 diffopts = patch.diffallopts(self.ui) |
468 diffopts = patch.diffallopts(self.ui) |
469 diffopts.showfunc = self.showfunc |
469 diffopts.showfunc = self.showfunc |
470 chunks = patch.diff(self.repo, prev, ref, opts=diffopts) |
470 chunks = patch.diff(self.repo, prev, ref, opts=diffopts) |
471 difflines = ''.join(chunks).splitlines() |
471 difflines = b''.join(chunks).splitlines() |
472 |
472 |
473 if self.ui.configbool('notify', 'diffstat'): |
473 if self.ui.configbool(b'notify', b'diffstat'): |
474 maxdiffstat = int(self.ui.config('notify', 'maxdiffstat')) |
474 maxdiffstat = int(self.ui.config(b'notify', b'maxdiffstat')) |
475 s = patch.diffstat(difflines) |
475 s = patch.diffstat(difflines) |
476 # s may be nil, don't include the header if it is |
476 # s may be nil, don't include the header if it is |
477 if s: |
477 if s: |
478 if maxdiffstat >= 0 and s.count("\n") > maxdiffstat + 1: |
478 if maxdiffstat >= 0 and s.count(b"\n") > maxdiffstat + 1: |
479 s = s.split("\n") |
479 s = s.split(b"\n") |
480 msg = _('\ndiffstat (truncated from %d to %d lines):\n\n') |
480 msg = _(b'\ndiffstat (truncated from %d to %d lines):\n\n') |
481 self.ui.write(msg % (len(s) - 2, maxdiffstat)) |
481 self.ui.write(msg % (len(s) - 2, maxdiffstat)) |
482 self.ui.write("\n".join(s[:maxdiffstat] + s[-2:])) |
482 self.ui.write(b"\n".join(s[:maxdiffstat] + s[-2:])) |
483 else: |
483 else: |
484 self.ui.write(_('\ndiffstat:\n\n%s') % s) |
484 self.ui.write(_(b'\ndiffstat:\n\n%s') % s) |
485 |
485 |
486 if maxdiff == 0: |
486 if maxdiff == 0: |
487 return |
487 return |
488 elif maxdiff > 0 and len(difflines) > maxdiff: |
488 elif maxdiff > 0 and len(difflines) > maxdiff: |
489 msg = _('\ndiffs (truncated from %d to %d lines):\n\n') |
489 msg = _(b'\ndiffs (truncated from %d to %d lines):\n\n') |
490 self.ui.write(msg % (len(difflines), maxdiff)) |
490 self.ui.write(msg % (len(difflines), maxdiff)) |
491 difflines = difflines[:maxdiff] |
491 difflines = difflines[:maxdiff] |
492 elif difflines: |
492 elif difflines: |
493 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines)) |
493 self.ui.write(_(b'\ndiffs (%d lines):\n\n') % len(difflines)) |
494 |
494 |
495 self.ui.write("\n".join(difflines)) |
495 self.ui.write(b"\n".join(difflines)) |
496 |
496 |
497 |
497 |
498 def hook(ui, repo, hooktype, node=None, source=None, **kwargs): |
498 def hook(ui, repo, hooktype, node=None, source=None, **kwargs): |
499 '''send email notifications to interested subscribers. |
499 '''send email notifications to interested subscribers. |
500 |
500 |
503 |
503 |
504 n = notifier(ui, repo, hooktype) |
504 n = notifier(ui, repo, hooktype) |
505 ctx = repo.unfiltered()[node] |
505 ctx = repo.unfiltered()[node] |
506 |
506 |
507 if not n.subs: |
507 if not n.subs: |
508 ui.debug('notify: no subscribers to repository %s\n' % n.root) |
508 ui.debug(b'notify: no subscribers to repository %s\n' % n.root) |
509 return |
509 return |
510 if n.skipsource(source): |
510 if n.skipsource(source): |
511 ui.debug('notify: changes have source "%s" - skipping\n' % source) |
511 ui.debug(b'notify: changes have source "%s" - skipping\n' % source) |
512 return |
512 return |
513 |
513 |
514 ui.pushbuffer() |
514 ui.pushbuffer() |
515 data = '' |
515 data = b'' |
516 count = 0 |
516 count = 0 |
517 author = '' |
517 author = b'' |
518 if hooktype == 'changegroup' or hooktype == 'outgoing': |
518 if hooktype == b'changegroup' or hooktype == b'outgoing': |
519 for rev in repo.changelog.revs(start=ctx.rev()): |
519 for rev in repo.changelog.revs(start=ctx.rev()): |
520 if n.node(repo[rev]): |
520 if n.node(repo[rev]): |
521 count += 1 |
521 count += 1 |
522 if not author: |
522 if not author: |
523 author = repo[rev].user() |
523 author = repo[rev].user() |
524 else: |
524 else: |
525 data += ui.popbuffer() |
525 data += ui.popbuffer() |
526 ui.note( |
526 ui.note( |
527 _('notify: suppressing notification for merge %d:%s\n') |
527 _(b'notify: suppressing notification for merge %d:%s\n') |
528 % (rev, repo[rev].hex()[:12]) |
528 % (rev, repo[rev].hex()[:12]) |
529 ) |
529 ) |
530 ui.pushbuffer() |
530 ui.pushbuffer() |
531 if count: |
531 if count: |
532 n.diff(ctx, repo['tip']) |
532 n.diff(ctx, repo[b'tip']) |
533 elif ctx.rev() in repo: |
533 elif ctx.rev() in repo: |
534 if not n.node(ctx): |
534 if not n.node(ctx): |
535 ui.popbuffer() |
535 ui.popbuffer() |
536 ui.note( |
536 ui.note( |
537 _('notify: suppressing notification for merge %d:%s\n') |
537 _(b'notify: suppressing notification for merge %d:%s\n') |
538 % (ctx.rev(), ctx.hex()[:12]) |
538 % (ctx.rev(), ctx.hex()[:12]) |
539 ) |
539 ) |
540 return |
540 return |
541 count += 1 |
541 count += 1 |
542 n.diff(ctx) |
542 n.diff(ctx) |
543 if not author: |
543 if not author: |
544 author = ctx.user() |
544 author = ctx.user() |
545 |
545 |
546 data += ui.popbuffer() |
546 data += ui.popbuffer() |
547 fromauthor = ui.config('notify', 'fromauthor') |
547 fromauthor = ui.config(b'notify', b'fromauthor') |
548 if author and fromauthor: |
548 if author and fromauthor: |
549 data = '\n'.join(['From: %s' % author, data]) |
549 data = b'\n'.join([b'From: %s' % author, data]) |
550 |
550 |
551 if count: |
551 if count: |
552 n.send(ctx, count, data) |
552 n.send(ctx, count, data) |
553 |
553 |
554 |
554 |