hgext/hgcia.py
author Patrick Mezard <patrick@mezard.eu>
Tue, 08 May 2012 22:43:44 +0200
changeset 16777 058e14da7044
parent 16743 38caf405d010
child 17424 e7cfe3587ea4
permissions -rw-r--r--
graphlog: turn getlogrevs() into a generator This improves the poor "time to first changeset" compared to the original log command. When running: $ hg log -u user log will enumerate the changelog and display matching revisions when they are found. But: $ hg log -G -u user will first find all revisions matching the user then start to display them. Initially, I considered turning revset.match() into a generator. This is doable but requires a fair amount of work. Instead, cmdutil.increasingwindows() is reused to call the revset matcher repeatedly. This has the nice properties of: - Let us reorder the windows after filtering, which is necessary as the matcher can reorder inputs but is an internal detail not a feature. - Let us feed the matcher with windows in changelog order, which is good for performances. - Have a generator designed for log-like commands, returning small windows at first then batching larger ones. I feel that calling the matcher multiple times is correct, at least with the revsets involved in getlogrevs() because they are: - stateless (no limit()) - respecting f(a|b) = f(a) | f(b), though I have no valid argument about that. Known issues compared to log code: - Calling the revset matcher multiple times can be slow when revset functions have to create expensive data structure for filtering. This will be addressed in a followup. - Predicate combinations like "--user foo --user bar" or "--user foo and --branch bar" are inherently slower because all input revision are checked against the first condition, then against the second, and so forth. log would enumerate the input revisions once and check each of them once against all conditions, which is faster. There are solutions but nothing cheap to implement. Some numbers against mozilla repository: first line total * hg log -u rnewman /Users/pmezard/bin/hg-2.2 0.148s 7.293s /Users/pmezard/bin/hgdev 0.132s 5.747s * hg log -u rnewman -u girard /Users/pmezard/bin/hg-2.2 0.146s 7.323s /Users/pmezard/bin/hgdev 0.136s 11.096s * hg log -l 10 /Users/pmezard/bin/hg-2.2 0.137s 0.153s /Users/pmezard/bin/hgdev 0.128s 0.144s * hg log -l 10 -u rnewman /Users/pmezard/bin/hg-2.2 0.146s 0.265s /Users/pmezard/bin/hgdev 0.133s 0.236s * hg log -b GECKO193a2_20100228_RELBRANCH /Users/pmezard/bin/hg-2.2 2.332s 6.618s /Users/pmezard/bin/hgdev 1.972s 5.543s * hg log xulrunner /Users/pmezard/bin/hg-2.2 5.829s 5.958s /Users/pmezard/bin/hgdev 0.194s 6.017s * hg log --follow xulrunner/build.mk /Users/pmezard/bin/hg-2.2 0.353s 0.438s /Users/pmezard/bin/hgdev 0.394s 0.580s * hg log -u girard tools /Users/pmezard/bin/hg-2.2 5.853s 6.012s /Users/pmezard/bin/hgdev 0.195s 6.030s * hg log -b COMM2000_20110314_RELBRANCH --copies /Users/pmezard/bin/hg-2.2 2.231s 6.653s /Users/pmezard/bin/hgdev 1.897s 5.585s * hg log --follow /Users/pmezard/bin/hg-2.2 0.137s 14.140s /Users/pmezard/bin/hgdev 0.381s 44.246s * hg log --follow -r 80000:90000 /Users/pmezard/bin/hg-2.2 0.127s 1.611s /Users/pmezard/bin/hgdev 0.147s 1.847s * hg log --follow -r 90000:80000 /Users/pmezard/bin/hg-2.2 0.130s 1.702s /Users/pmezard/bin/hgdev 0.368s 6.106s * hg log --follow -r 80000:90000 js/src/jsproxy.cpp /Users/pmezard/bin/hg-2.2 0.343s 0.388s /Users/pmezard/bin/hgdev 0.437s 0.631s * hg log --follow -r 90000:80000 js/src/jsproxy.cpp /Users/pmezard/bin/hg-2.2 0.342s 0.389s /Users/pmezard/bin/hgdev 0.442s 0.628s

# Copyright (C) 2007-8 Brendan Cully <brendan@kublai.com>
#
# 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 integrating with the CIA.vc notification service

This is meant to be run as a changegroup or incoming hook. To
configure it, set the following options in your hgrc::

  [cia]
  # your registered CIA user name
  user = foo
  # the name of the project in CIA
  project = foo
  # the module (subproject) (optional)
  #module = foo
  # Append a diffstat to the log message (optional)
  #diffstat = False
  # Template to use for log messages (optional)
  #template = {desc}\\n{baseurl}{webroot}/rev/{node}-- {diffstat}
  # Style to use (optional)
  #style = foo
  # The URL of the CIA notification service (optional)
  # You can use mailto: URLs to send by email, eg
  # mailto:cia@cia.vc
  # Make sure to set email.from if you do this.
  #url = http://cia.vc/
  # print message instead of sending it (optional)
  #test = False
  # number of slashes to strip for url paths
  #strip = 0

  [hooks]
  # one of these:
  changegroup.cia = python:hgcia.hook
  #incoming.cia = python:hgcia.hook

  [web]
  # If you want hyperlinks (optional)
  baseurl = http://server/path/to/repo
"""

from mercurial.i18n import _
from mercurial.node import bin, short
from mercurial import cmdutil, patch, templater, util, mail
import email.Parser

import socket, xmlrpclib
from xml.sax import saxutils
testedwith = 'internal'

socket_timeout = 30 # seconds
if util.safehasattr(socket, 'setdefaulttimeout'):
    # set a timeout for the socket so you don't have to wait so looooong
    # when cia.vc is having problems. requires python >= 2.3:
    socket.setdefaulttimeout(socket_timeout)

HGCIA_VERSION = '0.1'
HGCIA_URL = 'http://hg.kublai.com/mercurial/hgcia'


class ciamsg(object):
    """ A CIA message """
    def __init__(self, cia, ctx):
        self.cia = cia
        self.ctx = ctx
        self.url = self.cia.url
        if self.url:
            self.url += self.cia.root

    def fileelem(self, path, uri, action):
        if uri:
            uri = ' uri=%s' % saxutils.quoteattr(uri)
        return '<file%s action=%s>%s</file>' % (
            uri, saxutils.quoteattr(action), saxutils.escape(path))

    def fileelems(self):
        n = self.ctx.node()
        f = self.cia.repo.status(self.ctx.p1().node(), n)
        url = self.url or ''
        if url and url[-1] == '/':
            url = url[:-1]
        elems = []
        for path in f[0]:
            uri = '%s/diff/%s/%s' % (url, short(n), path)
            elems.append(self.fileelem(path, url and uri, 'modify'))
        for path in f[1]:
            # TODO: copy/rename ?
            uri = '%s/file/%s/%s' % (url, short(n), path)
            elems.append(self.fileelem(path, url and uri, 'add'))
        for path in f[2]:
            elems.append(self.fileelem(path, '', 'remove'))

        return '\n'.join(elems)

    def sourceelem(self, project, module=None, branch=None):
        msg = ['<source>', '<project>%s</project>' % saxutils.escape(project)]
        if module:
            msg.append('<module>%s</module>' % saxutils.escape(module))
        if branch:
            msg.append('<branch>%s</branch>' % saxutils.escape(branch))
        msg.append('</source>')

        return '\n'.join(msg)

    def diffstat(self):
        class patchbuf(object):
            def __init__(self):
                self.lines = []
                # diffstat is stupid
                self.name = 'cia'
            def write(self, data):
                self.lines += data.splitlines(True)
            def close(self):
                pass

        n = self.ctx.node()
        pbuf = patchbuf()
        cmdutil.export(self.cia.repo, [n], fp=pbuf)
        return patch.diffstat(pbuf.lines) or ''

    def logmsg(self):
        diffstat = self.cia.diffstat and self.diffstat() or ''
        self.cia.ui.pushbuffer()
        self.cia.templater.show(self.ctx, changes=self.ctx.changeset(),
                                baseurl=self.cia.ui.config('web', 'baseurl'),
                                url=self.url, diffstat=diffstat,
                                webroot=self.cia.root)
        return self.cia.ui.popbuffer()

    def xml(self):
        n = short(self.ctx.node())
        src = self.sourceelem(self.cia.project, module=self.cia.module,
                              branch=self.ctx.branch())
        # unix timestamp
        dt = self.ctx.date()
        timestamp = dt[0]

        author = saxutils.escape(self.ctx.user())
        rev = '%d:%s' % (self.ctx.rev(), n)
        log = saxutils.escape(self.logmsg())

        url = self.url
        if url and url[-1] == '/':
            url = url[:-1]
        url = url and '<url>%s/rev/%s</url>' % (saxutils.escape(url), n) or ''

        msg = """
<message>
  <generator>
    <name>Mercurial (hgcia)</name>
    <version>%s</version>
    <url>%s</url>
    <user>%s</user>
  </generator>
  %s
  <body>
    <commit>
      <author>%s</author>
      <version>%s</version>
      <log>%s</log>
      %s
      <files>%s</files>
    </commit>
  </body>
  <timestamp>%d</timestamp>
</message>
""" % \
            (HGCIA_VERSION, saxutils.escape(HGCIA_URL),
            saxutils.escape(self.cia.user), src, author, rev, log, url,
            self.fileelems(), timestamp)

        return msg


class hgcia(object):
    """ CIA notification class """

    deftemplate = '{desc}'
    dstemplate = '{desc}\n-- \n{diffstat}'

    def __init__(self, ui, repo):
        self.ui = ui
        self.repo = repo

        self.ciaurl = self.ui.config('cia', 'url', 'http://cia.vc')
        self.user = self.ui.config('cia', 'user')
        self.project = self.ui.config('cia', 'project')
        self.module = self.ui.config('cia', 'module')
        self.diffstat = self.ui.configbool('cia', 'diffstat')
        self.emailfrom = self.ui.config('email', 'from')
        self.dryrun = self.ui.configbool('cia', 'test')
        self.url = self.ui.config('web', 'baseurl')
        # Default to -1 for backward compatibility
        self.stripcount = int(self.ui.config('cia', 'strip', -1))
        self.root = self.strip(self.repo.root)

        style = self.ui.config('cia', 'style')
        template = self.ui.config('cia', 'template')
        if not template:
            template = self.diffstat and self.dstemplate or self.deftemplate
        template = templater.parsestring(template, quoted=False)
        t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
                                        style, False)
        t.use_template(template)
        self.templater = t

    def strip(self, path):
        '''strip leading slashes from local path, turn into web-safe path.'''

        path = util.pconvert(path)
        count = self.stripcount
        if count < 0:
            return ''
        while count > 0:
            c = path.find('/')
            if c == -1:
                break
            path = path[c + 1:]
            count -= 1
        return path

    def sendrpc(self, msg):
        srv = xmlrpclib.Server(self.ciaurl)
        res = srv.hub.deliver(msg)
        if res is not True and res != 'queued.':
            raise util.Abort(_('%s returned an error: %s') %
                             (self.ciaurl, res))

    def sendemail(self, address, data):
        p = email.Parser.Parser()
        msg = p.parsestr(data)
        msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
        msg['To'] = address
        msg['From'] = self.emailfrom
        msg['Subject'] = 'DeliverXML'
        msg['Content-type'] = 'text/xml'
        msgtext = msg.as_string()

        self.ui.status(_('hgcia: sending update to %s\n') % address)
        mail.sendmail(self.ui, util.email(self.emailfrom),
                      [address], msgtext)


def hook(ui, repo, hooktype, node=None, url=None, **kwargs):
    """ send CIA notification """
    def sendmsg(cia, ctx):
        msg = ciamsg(cia, ctx).xml()
        if cia.dryrun:
            ui.write(msg)
        elif cia.ciaurl.startswith('mailto:'):
            if not cia.emailfrom:
                raise util.Abort(_('email.from must be defined when '
                                   'sending by email'))
            cia.sendemail(cia.ciaurl[7:], msg)
        else:
            cia.sendrpc(msg)

    n = bin(node)
    cia = hgcia(ui, repo)
    if not cia.user:
        ui.debug('cia: no user specified')
        return
    if not cia.project:
        ui.debug('cia: no project specified')
        return
    if hooktype == 'changegroup':
        start = repo.changelog.rev(n)
        end = len(repo.changelog)
        for rev in xrange(start, end):
            n = repo.changelog.node(rev)
            ctx = repo.changectx(n)
            sendmsg(cia, ctx)
    else:
        ctx = repo.changectx(n)
        sendmsg(cia, ctx)