diff -r 5ff1fc726848 -r c6bca38c1cbf eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/notify.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/notify.py Sat Jan 08 11:20:57 2011 +0530 @@ -0,0 +1,316 @@ +# notify.py - email notifications for mercurial +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''hooks for sending email notifications at commit/push time + +Subscriptions can be managed through a hgrc file. Default mode is to +print messages to stdout, for testing and configuring. + +To use, configure the notify extension and enable it in hgrc like +this:: + + [extensions] + notify = + + [hooks] + # one email for each incoming changeset + incoming.notify = python:hgext.notify.hook + # batch emails when many changesets incoming at one time + changegroup.notify = python:hgext.notify.hook + + [notify] + # config items go here + +Required configuration items:: + + config = /path/to/file # file containing subscriptions + +Optional configuration items:: + + test = True # print messages to stdout for testing + strip = 3 # number of slashes to strip for url paths + domain = example.com # domain to use if committer missing domain + style = ... # style file to use when formatting email + template = ... # template to use when formatting email + incoming = ... # template to use when run as incoming hook + changegroup = ... # template when run as changegroup hook + maxdiff = 300 # max lines of diffs to include (0=none, -1=all) + maxsubject = 67 # truncate subject line longer than this + diffstat = True # add a diffstat before the diff content + sources = serve # notify if source of incoming changes in this list + # (serve == ssh or http, push, pull, bundle) + merge = False # send notification for merges (default True) + [email] + from = user@host.com # email address to send as if none given + [web] + baseurl = http://hgserver/... # root of hg web site for browsing commits + +The notify config file has same format as a regular hgrc file. It has +two sections so you can express subscriptions in whatever way is +handier for you. + +:: + + [usersubs] + # key is subscriber email, value is ","-separated list of glob patterns + user@host = pattern + + [reposubs] + # key is glob pattern, value is ","-separated list of subscriber emails + pattern = user@host + +Glob patterns are matched against path to repository root. + +If you like, you can put notify config file in repository that users +can push changes to, they can manage their own subscriptions. +''' + +from mercurial.i18n import _ +from mercurial import patch, cmdutil, templater, util, mail +import email.Parser, email.Errors, fnmatch, socket, time + +# template for single changeset can include email headers. +single_template = ''' +Subject: changeset in {webroot}: {desc|firstline|strip} +From: {author} + +changeset {node|short} in {root} +details: {baseurl}{webroot}?cmd=changeset;node={node|short} +description: +\t{desc|tabindent|strip} +'''.lstrip() + +# template for multiple changesets should not contain email headers, +# because only first set of headers will be used and result will look +# strange. +multiple_template = ''' +changeset {node|short} in {root} +details: {baseurl}{webroot}?cmd=changeset;node={node|short} +summary: {desc|firstline} +''' + +deftemplates = { + 'changegroup': multiple_template, +} + +class notifier(object): + '''email notification class.''' + + def __init__(self, ui, repo, hooktype): + self.ui = ui + cfg = self.ui.config('notify', 'config') + if cfg: + self.ui.readconfig(cfg, sections=['usersubs', 'reposubs']) + self.repo = repo + self.stripcount = int(self.ui.config('notify', 'strip', 0)) + self.root = self.strip(self.repo.root) + self.domain = self.ui.config('notify', 'domain') + self.test = self.ui.configbool('notify', 'test', True) + self.charsets = mail._charsets(self.ui) + self.subs = self.subscribers() + self.merge = self.ui.configbool('notify', 'merge', True) + + mapfile = self.ui.config('notify', 'style') + template = (self.ui.config('notify', hooktype) or + self.ui.config('notify', 'template')) + self.t = cmdutil.changeset_templater(self.ui, self.repo, + False, None, mapfile, False) + if not mapfile and not template: + template = deftemplates.get(hooktype) or single_template + if template: + template = templater.parsestring(template, quoted=False) + self.t.use_template(template) + + def strip(self, path): + '''strip leading slashes from local path, turn into web-safe path.''' + + path = util.pconvert(path) + count = self.stripcount + while count > 0: + c = path.find('/') + if c == -1: + break + path = path[c + 1:] + count -= 1 + return path + + def fixmail(self, addr): + '''try to clean up email addresses.''' + + addr = util.email(addr.strip()) + if self.domain: + a = addr.find('@localhost') + if a != -1: + addr = addr[:a] + if '@' not in addr: + return addr + '@' + self.domain + return addr + + def subscribers(self): + '''return list of email addresses of subscribers to this repo.''' + subs = set() + for user, pats in self.ui.configitems('usersubs'): + for pat in pats.split(','): + if fnmatch.fnmatch(self.repo.root, pat.strip()): + subs.add(self.fixmail(user)) + for pat, users in self.ui.configitems('reposubs'): + if fnmatch.fnmatch(self.repo.root, pat): + for user in users.split(','): + subs.add(self.fixmail(user)) + return [mail.addressencode(self.ui, s, self.charsets, self.test) + for s in sorted(subs)] + + def url(self, path=None): + return self.ui.config('web', 'baseurl') + (path or self.root) + + def node(self, ctx, **props): + '''format one changeset, unless it is a suppressed merge.''' + if not self.merge and len(ctx.parents()) > 1: + return False + self.t.show(ctx, changes=ctx.changeset(), + baseurl=self.ui.config('web', 'baseurl'), + root=self.repo.root, webroot=self.root, **props) + return True + + def skipsource(self, source): + '''true if incoming changes from this source should be skipped.''' + ok_sources = self.ui.config('notify', 'sources', 'serve').split() + return source not in ok_sources + + def send(self, ctx, count, data): + '''send message.''' + + p = email.Parser.Parser() + try: + msg = p.parsestr(data) + except email.Errors.MessageParseError, inst: + raise util.Abort(inst) + + # store sender and subject + sender, subject = msg['From'], msg['Subject'] + del msg['From'], msg['Subject'] + + if not msg.is_multipart(): + # create fresh mime message from scratch + # (multipart templates must take care of this themselves) + headers = msg.items() + payload = msg.get_payload() + # for notification prefer readability over data precision + msg = mail.mimeencode(self.ui, payload, self.charsets, self.test) + # reinstate custom headers + for k, v in headers: + msg[k] = v + + msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2") + + # try to make subject line exist and be useful + if not subject: + if count > 1: + subject = _('%s: %d new changesets') % (self.root, count) + else: + s = ctx.description().lstrip().split('\n', 1)[0].rstrip() + subject = '%s: %s' % (self.root, s) + maxsubject = int(self.ui.config('notify', 'maxsubject', 67)) + if maxsubject: + subject = util.ellipsis(subject, maxsubject) + msg['Subject'] = mail.headencode(self.ui, subject, + self.charsets, self.test) + + # try to make message have proper sender + if not sender: + sender = self.ui.config('email', 'from') or self.ui.username() + if '@' not in sender or '@localhost' in sender: + sender = self.fixmail(sender) + msg['From'] = mail.addressencode(self.ui, sender, + self.charsets, self.test) + + msg['X-Hg-Notification'] = 'changeset %s' % ctx + if not msg['Message-Id']: + msg['Message-Id'] = ('' % + (ctx, int(time.time()), + hash(self.repo.root), socket.getfqdn())) + msg['To'] = ', '.join(self.subs) + + msgtext = msg.as_string() + if self.test: + self.ui.write(msgtext) + if not msgtext.endswith('\n'): + self.ui.write('\n') + else: + self.ui.status(_('notify: sending %d subscribers %d changes\n') % + (len(self.subs), count)) + mail.sendmail(self.ui, util.email(msg['From']), + self.subs, msgtext) + + def diff(self, ctx, ref=None): + + maxdiff = int(self.ui.config('notify', 'maxdiff', 300)) + prev = ctx.parents()[0].node() + ref = ref and ref.node() or ctx.node() + chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui)) + difflines = ''.join(chunks).splitlines() + + if self.ui.configbool('notify', 'diffstat', True): + s = patch.diffstat(difflines) + # s may be nil, don't include the header if it is + if s: + self.ui.write('\ndiffstat:\n\n%s' % s) + + if maxdiff == 0: + return + elif maxdiff > 0 and len(difflines) > maxdiff: + msg = _('\ndiffs (truncated from %d to %d lines):\n\n') + self.ui.write(msg % (len(difflines), maxdiff)) + difflines = difflines[:maxdiff] + elif difflines: + self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines)) + + self.ui.write("\n".join(difflines)) + +def hook(ui, repo, hooktype, node=None, source=None, **kwargs): + '''send email notifications to interested subscribers. + + if used as changegroup hook, send one email for all changesets in + changegroup. else send one email per changeset.''' + + n = notifier(ui, repo, hooktype) + ctx = repo[node] + + if not n.subs: + ui.debug('notify: no subscribers to repository %s\n' % n.root) + return + if n.skipsource(source): + ui.debug('notify: changes have source "%s" - skipping\n' % source) + return + + ui.pushbuffer() + data = '' + count = 0 + if hooktype == 'changegroup': + start, end = ctx.rev(), len(repo) + for rev in xrange(start, end): + if n.node(repo[rev]): + count += 1 + else: + data += ui.popbuffer() + ui.note(_('notify: suppressing notification for merge %d:%s\n') % + (rev, repo[rev].hex()[:12])) + ui.pushbuffer() + if count: + n.diff(ctx, repo['tip']) + else: + if not n.node(ctx): + ui.popbuffer() + ui.note(_('notify: suppressing notification for merge %d:%s\n') % + (ctx.rev(), ctx.hex()[:12])) + return + count += 1 + n.diff(ctx) + + data += ui.popbuffer() + if count: + n.send(ctx, count, data)