eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/notify.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # notify.py - email notifications for mercurial
       
     2 #
       
     3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 '''hooks for sending email notifications at commit/push time
       
     9 
       
    10 Subscriptions can be managed through a hgrc file. Default mode is to
       
    11 print messages to stdout, for testing and configuring.
       
    12 
       
    13 To use, configure the notify extension and enable it in hgrc like
       
    14 this::
       
    15 
       
    16   [extensions]
       
    17   notify =
       
    18 
       
    19   [hooks]
       
    20   # one email for each incoming changeset
       
    21   incoming.notify = python:hgext.notify.hook
       
    22   # batch emails when many changesets incoming at one time
       
    23   changegroup.notify = python:hgext.notify.hook
       
    24 
       
    25   [notify]
       
    26   # config items go here
       
    27 
       
    28 Required configuration items::
       
    29 
       
    30   config = /path/to/file # file containing subscriptions
       
    31 
       
    32 Optional configuration items::
       
    33 
       
    34   test = True            # print messages to stdout for testing
       
    35   strip = 3              # number of slashes to strip for url paths
       
    36   domain = example.com   # domain to use if committer missing domain
       
    37   style = ...            # style file to use when formatting email
       
    38   template = ...         # template to use when formatting email
       
    39   incoming = ...         # template to use when run as incoming hook
       
    40   changegroup = ...      # template when run as changegroup hook
       
    41   maxdiff = 300          # max lines of diffs to include (0=none, -1=all)
       
    42   maxsubject = 67        # truncate subject line longer than this
       
    43   diffstat = True        # add a diffstat before the diff content
       
    44   sources = serve        # notify if source of incoming changes in this list
       
    45                          # (serve == ssh or http, push, pull, bundle)
       
    46   merge = False          # send notification for merges (default True)
       
    47   [email]
       
    48   from = user@host.com   # email address to send as if none given
       
    49   [web]
       
    50   baseurl = http://hgserver/... # root of hg web site for browsing commits
       
    51 
       
    52 The notify config file has same format as a regular hgrc file. It has
       
    53 two sections so you can express subscriptions in whatever way is
       
    54 handier for you.
       
    55 
       
    56 ::
       
    57 
       
    58   [usersubs]
       
    59   # key is subscriber email, value is ","-separated list of glob patterns
       
    60   user@host = pattern
       
    61 
       
    62   [reposubs]
       
    63   # key is glob pattern, value is ","-separated list of subscriber emails
       
    64   pattern = user@host
       
    65 
       
    66 Glob patterns are matched against path to repository root.
       
    67 
       
    68 If you like, you can put notify config file in repository that users
       
    69 can push changes to, they can manage their own subscriptions.
       
    70 '''
       
    71 
       
    72 from mercurial.i18n import _
       
    73 from mercurial import patch, cmdutil, templater, util, mail
       
    74 import email.Parser, email.Errors, fnmatch, socket, time
       
    75 
       
    76 # template for single changeset can include email headers.
       
    77 single_template = '''
       
    78 Subject: changeset in {webroot}: {desc|firstline|strip}
       
    79 From: {author}
       
    80 
       
    81 changeset {node|short} in {root}
       
    82 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
       
    83 description:
       
    84 \t{desc|tabindent|strip}
       
    85 '''.lstrip()
       
    86 
       
    87 # template for multiple changesets should not contain email headers,
       
    88 # because only first set of headers will be used and result will look
       
    89 # strange.
       
    90 multiple_template = '''
       
    91 changeset {node|short} in {root}
       
    92 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
       
    93 summary: {desc|firstline}
       
    94 '''
       
    95 
       
    96 deftemplates = {
       
    97     'changegroup': multiple_template,
       
    98 }
       
    99 
       
   100 class notifier(object):
       
   101     '''email notification class.'''
       
   102 
       
   103     def __init__(self, ui, repo, hooktype):
       
   104         self.ui = ui
       
   105         cfg = self.ui.config('notify', 'config')
       
   106         if cfg:
       
   107             self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
       
   108         self.repo = repo
       
   109         self.stripcount = int(self.ui.config('notify', 'strip', 0))
       
   110         self.root = self.strip(self.repo.root)
       
   111         self.domain = self.ui.config('notify', 'domain')
       
   112         self.test = self.ui.configbool('notify', 'test', True)
       
   113         self.charsets = mail._charsets(self.ui)
       
   114         self.subs = self.subscribers()
       
   115         self.merge = self.ui.configbool('notify', 'merge', True)
       
   116 
       
   117         mapfile = self.ui.config('notify', 'style')
       
   118         template = (self.ui.config('notify', hooktype) or
       
   119                     self.ui.config('notify', 'template'))
       
   120         self.t = cmdutil.changeset_templater(self.ui, self.repo,
       
   121                                              False, None, mapfile, False)
       
   122         if not mapfile and not template:
       
   123             template = deftemplates.get(hooktype) or single_template
       
   124         if template:
       
   125             template = templater.parsestring(template, quoted=False)
       
   126             self.t.use_template(template)
       
   127 
       
   128     def strip(self, path):
       
   129         '''strip leading slashes from local path, turn into web-safe path.'''
       
   130 
       
   131         path = util.pconvert(path)
       
   132         count = self.stripcount
       
   133         while count > 0:
       
   134             c = path.find('/')
       
   135             if c == -1:
       
   136                 break
       
   137             path = path[c + 1:]
       
   138             count -= 1
       
   139         return path
       
   140 
       
   141     def fixmail(self, addr):
       
   142         '''try to clean up email addresses.'''
       
   143 
       
   144         addr = util.email(addr.strip())
       
   145         if self.domain:
       
   146             a = addr.find('@localhost')
       
   147             if a != -1:
       
   148                 addr = addr[:a]
       
   149             if '@' not in addr:
       
   150                 return addr + '@' + self.domain
       
   151         return addr
       
   152 
       
   153     def subscribers(self):
       
   154         '''return list of email addresses of subscribers to this repo.'''
       
   155         subs = set()
       
   156         for user, pats in self.ui.configitems('usersubs'):
       
   157             for pat in pats.split(','):
       
   158                 if fnmatch.fnmatch(self.repo.root, pat.strip()):
       
   159                     subs.add(self.fixmail(user))
       
   160         for pat, users in self.ui.configitems('reposubs'):
       
   161             if fnmatch.fnmatch(self.repo.root, pat):
       
   162                 for user in users.split(','):
       
   163                     subs.add(self.fixmail(user))
       
   164         return [mail.addressencode(self.ui, s, self.charsets, self.test)
       
   165                 for s in sorted(subs)]
       
   166 
       
   167     def url(self, path=None):
       
   168         return self.ui.config('web', 'baseurl') + (path or self.root)
       
   169 
       
   170     def node(self, ctx, **props):
       
   171         '''format one changeset, unless it is a suppressed merge.'''
       
   172         if not self.merge and len(ctx.parents()) > 1:
       
   173             return False
       
   174         self.t.show(ctx, changes=ctx.changeset(),
       
   175                     baseurl=self.ui.config('web', 'baseurl'),
       
   176                     root=self.repo.root, webroot=self.root, **props)
       
   177         return True
       
   178 
       
   179     def skipsource(self, source):
       
   180         '''true if incoming changes from this source should be skipped.'''
       
   181         ok_sources = self.ui.config('notify', 'sources', 'serve').split()
       
   182         return source not in ok_sources
       
   183 
       
   184     def send(self, ctx, count, data):
       
   185         '''send message.'''
       
   186 
       
   187         p = email.Parser.Parser()
       
   188         try:
       
   189             msg = p.parsestr(data)
       
   190         except email.Errors.MessageParseError, inst:
       
   191             raise util.Abort(inst)
       
   192 
       
   193         # store sender and subject
       
   194         sender, subject = msg['From'], msg['Subject']
       
   195         del msg['From'], msg['Subject']
       
   196 
       
   197         if not msg.is_multipart():
       
   198             # create fresh mime message from scratch
       
   199             # (multipart templates must take care of this themselves)
       
   200             headers = msg.items()
       
   201             payload = msg.get_payload()
       
   202             # for notification prefer readability over data precision
       
   203             msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
       
   204             # reinstate custom headers
       
   205             for k, v in headers:
       
   206                 msg[k] = v
       
   207 
       
   208         msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
       
   209 
       
   210         # try to make subject line exist and be useful
       
   211         if not subject:
       
   212             if count > 1:
       
   213                 subject = _('%s: %d new changesets') % (self.root, count)
       
   214             else:
       
   215                 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
       
   216                 subject = '%s: %s' % (self.root, s)
       
   217         maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
       
   218         if maxsubject:
       
   219             subject = util.ellipsis(subject, maxsubject)
       
   220         msg['Subject'] = mail.headencode(self.ui, subject,
       
   221                                          self.charsets, self.test)
       
   222 
       
   223         # try to make message have proper sender
       
   224         if not sender:
       
   225             sender = self.ui.config('email', 'from') or self.ui.username()
       
   226         if '@' not in sender or '@localhost' in sender:
       
   227             sender = self.fixmail(sender)
       
   228         msg['From'] = mail.addressencode(self.ui, sender,
       
   229                                          self.charsets, self.test)
       
   230 
       
   231         msg['X-Hg-Notification'] = 'changeset %s' % ctx
       
   232         if not msg['Message-Id']:
       
   233             msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
       
   234                                  (ctx, int(time.time()),
       
   235                                   hash(self.repo.root), socket.getfqdn()))
       
   236         msg['To'] = ', '.join(self.subs)
       
   237 
       
   238         msgtext = msg.as_string()
       
   239         if self.test:
       
   240             self.ui.write(msgtext)
       
   241             if not msgtext.endswith('\n'):
       
   242                 self.ui.write('\n')
       
   243         else:
       
   244             self.ui.status(_('notify: sending %d subscribers %d changes\n') %
       
   245                            (len(self.subs), count))
       
   246             mail.sendmail(self.ui, util.email(msg['From']),
       
   247                           self.subs, msgtext)
       
   248 
       
   249     def diff(self, ctx, ref=None):
       
   250 
       
   251         maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
       
   252         prev = ctx.parents()[0].node()
       
   253         ref = ref and ref.node() or ctx.node()
       
   254         chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
       
   255         difflines = ''.join(chunks).splitlines()
       
   256 
       
   257         if self.ui.configbool('notify', 'diffstat', True):
       
   258             s = patch.diffstat(difflines)
       
   259             # s may be nil, don't include the header if it is
       
   260             if s:
       
   261                 self.ui.write('\ndiffstat:\n\n%s' % s)
       
   262 
       
   263         if maxdiff == 0:
       
   264             return
       
   265         elif maxdiff > 0 and len(difflines) > maxdiff:
       
   266             msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
       
   267             self.ui.write(msg % (len(difflines), maxdiff))
       
   268             difflines = difflines[:maxdiff]
       
   269         elif difflines:
       
   270             self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
       
   271 
       
   272         self.ui.write("\n".join(difflines))
       
   273 
       
   274 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
       
   275     '''send email notifications to interested subscribers.
       
   276 
       
   277     if used as changegroup hook, send one email for all changesets in
       
   278     changegroup. else send one email per changeset.'''
       
   279 
       
   280     n = notifier(ui, repo, hooktype)
       
   281     ctx = repo[node]
       
   282 
       
   283     if not n.subs:
       
   284         ui.debug('notify: no subscribers to repository %s\n' % n.root)
       
   285         return
       
   286     if n.skipsource(source):
       
   287         ui.debug('notify: changes have source "%s" - skipping\n' % source)
       
   288         return
       
   289 
       
   290     ui.pushbuffer()
       
   291     data = ''
       
   292     count = 0
       
   293     if hooktype == 'changegroup':
       
   294         start, end = ctx.rev(), len(repo)
       
   295         for rev in xrange(start, end):
       
   296             if n.node(repo[rev]):
       
   297                 count += 1
       
   298             else:
       
   299                 data += ui.popbuffer()
       
   300                 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
       
   301                         (rev, repo[rev].hex()[:12]))
       
   302                 ui.pushbuffer()
       
   303         if count:
       
   304             n.diff(ctx, repo['tip'])
       
   305     else:
       
   306         if not n.node(ctx):
       
   307             ui.popbuffer()
       
   308             ui.note(_('notify: suppressing notification for merge %d:%s\n') %
       
   309                     (ctx.rev(), ctx.hex()[:12]))
       
   310             return
       
   311         count += 1
       
   312         n.diff(ctx)
       
   313 
       
   314     data += ui.popbuffer()
       
   315     if count:
       
   316         n.send(ctx, count, data)