savannah-hackers
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Savannah-help-public] [sr #107346] Please activate bzr commit notif


From: Karl Berry
Subject: Re: [Savannah-help-public] [sr #107346] Please activate bzr commit notification for gnue
Date: Tue, 16 Aug 2011 22:37:04 GMT

    It looks to me like it ought to work.  I filed
    <https://bugs.launchpad.net/bzr-hookless-email/+bug/827079>; I'll try
    to investigate it this afternoon.

FWIW, appended is the actual
/usr/src/bzr-hookless-email/bzr_hookless_email.py script as it exists on
the host.  In case local or Debian changes were made which are the
culprit.

The invocation was 
  /usr/src/bzr-hookless-email/bzr_hookless_email.py \
  -r -f -e address@hidden /srv/bzr/gnue -l 10000

Maybe we just have to give -r an argument, i.e., -r /srv/bzr/gnue?
Yeah, judging from the help message, I bet that is all it is.  Sorry.

I made that change (and commented out all the other invocations).
Reinhard, please try another commit in some branch.

If this is the right story, perhaps the bug report should morph into
"give error message when -r is not followed by a valid directory" ...

Thanks,
Karl


#! /usr/bin/env python
## vim: fileencoding=utf-8

# Copyright (c) 2007 Adeodato Simó (address@hidden)
# Licensed under the terms of the MIT license.

"""Send commit emails for Bazaar branches."""

import os
import sys
import socket
import optparse
import StringIO
import cStringIO

try:
    import pyinotify
    from pyinotify import EventsCodes
    _has_pyinotify = True
    _has_kernel_inotify = True
except ImportError:
    _has_pyinotify = False
    _has_kernel_inotify = None
except RuntimeError:
    _has_pyinotify = True
    _has_kernel_inotify = False

if not _has_pyinotify or not _has_kernel_inotify:
    class pyinotify:
        # need to have pyinotify.ProcessEvent available
        # for BranchEmailer to inherit from it
        class ProcessEvent:
            def __init__(self):
                pass

from bzrlib import revision as _mod_revision
from bzrlib import version_info as _bzrlib_version_info
from bzrlib.bzrdir import BzrDir
from bzrlib.diff import show_diff_trees
from bzrlib.errors import (NotBranchError, NoSuchRevision)
from bzrlib.log import log_formatter, show_log
from bzrlib.osutils import format_date

if _bzrlib_version_info >= (0, 19) and False: # we have local modifications
    from bzrlib.errors import SMTPError
    from bzrlib.email_message import EmailMessage
    from bzrlib.smtp_connection import SMTPConnection
else:
    from local_bzrlib.email_message import EmailMessage
    from local_bzrlib.smtp_connection import SMTPConnection, SMTPError

###

def main():
    global options
    options, arguments = parse_options()

    if not options.email:
        print >>sys.stderr, 'E: no destination address given.'
        sys.exit(1)

    if options.recurse_dirs:
        arguments.extend(find_branches(options.recurse_dirs))
        if not arguments:
            print >>sys.stderr, 'E: no branches found.'
            sys.exit(1)

    if not arguments:
        print >>sys.stderr, 'E: no branches given.'
        sys.exit(1)

    # TODO: chdir to the common prefix of all branches? (nicer subjects)

    branches = []

    for branch_path in arguments:
        try:
            branch = BranchEmailer(branch_path)
            branch.update()
            branches.append(branch)
        except NotBranchError:
            print >>sys.stderr, 'W: could not open %s, skipping.' % branch_path

    if not options.daemon:
        return
    elif not _has_pyinotify:
        print >>sys.stderr, \
            'E: module pyinotify not found for daemon mode.'
        sys.exit(1)
    elif not _has_kernel_inotify:
        print >>sys.stderr, \
            'E: pyinotify could not find inotify support in the kernel.'
        sys.exit(1)

    if options.bg_fork:
        daemonize(options.logfile)

    try:
        # pyinotify >= 0.7
        watch_manager = pyinotify.WatchManager()
        notifier = pyinotify.Notifier(watch_manager)
    except AttributeError:
        # pyinotify 0.5
        watch_manager = notifier = pyinotify.SimpleINotify()
        notifier.check_events = notifier.event_check # \o/

    for branch in branches:
        watch_manager.add_watch(os.path.join(branch.path, '.bzr/branch'),
                EventsCodes.IN_CREATE | EventsCodes.IN_MODIFY |
                EventsCodes.IN_MOVED_TO, branch)

    while True:
        notifier.check_events(timeout=None) # blocks
        notifier.read_events()
        notifier.process_events()

###

class BranchEmailer(pyinotify.ProcessEvent):

    _option_name = 'last_revision_mailed'

    def __init__(self, path):
        pyinotify.ProcessEvent.__init__(self)
        self.path = path
        self._branch = BzrDir.open_containing(path)[0].open_branch()
        self._config = self._branch.get_config()

    def update(self):
        smtp = SMTPConnection(self._config)
        smtp_from = None
        for revision in self._revisions_to_send():
            msg = self._compose_email(revision)
            try:
                smtp.send_email(msg)
            except SMTPError:
                # let's assume the server did not like the MAIL FROM
                try:
                    if smtp_from is None:
                        smtp_from = os.environ['LOGNAME'] + '@' + 
socket.getfqdn()
                    smtp.send_email(msg, smtp_from=smtp_from)
                except SMTPError, e:
                    print >>sys.stderr, \
                            'Could not send revision %s: %s' % (revision, e)
            # TODO: keep a list of failed revisions, and an option to "replay"
            self._config.set_user_option(self._option_name, revision)

    def _revisions_to_send(self):
        revision_history = self._branch.revision_history()
        last_mailed = self._config.get_user_option(self._option_name)

        if last_mailed is None:
            print >>sys.stderr, ('W: branch %s does not have %s set; setting it 
'
                'to the last available revision' % (self.path, 
self._option_name))
            # XXX: raises IndexError if watching an empty tree
            self._config.set_user_option(self._option_name, 
revision_history[-1])
            return []
        elif last_mailed not in revision_history:
            # The tip we last saw disappeared, either by uncommit or by having
            # been merged by a new mainline revision. So, we use as last_mailed
            # revision the first mainline ancestor of the disappeared one.
            revision = last_mailed.encode('utf-8')
            repo = self._branch.repository
            while True:
                try:
                    revobj = repo.get_revision(revision)
                except NoSuchRevision:
                    print >>sys.stderr, (
                        'E: %s does not exist in the repository for %s\n'
                        'E: setting %s to the last available revision' % (
                            last_mailed, self.path, self._option_name))
                    self._config.set_user_option(
                            self._option_name, revision_history[-1])
                    return []

                if revobj.parent_ids:
                    revision = revobj.parent_ids[0]
                    if revision in revision_history:
                        last_mailed = revision
                        break
                else:
                    return revision_history

        index = revision_history.index(last_mailed)
        return revision_history[index+1:]

    def _compose_email(self, revid):
        ### This code is based/stolen from that in bzr-email/emailer.py:
        ### Copyright (C) 2005, 2006, 2007 Canonical Ltd.
        ### Licensed under the terms of GPLv2 or later.
        rev1 = rev2 = self._branch.revision_id_to_revno(revid) or None
        revision = self._branch.repository.get_revision(revid)
        body = StringIO.StringIO()

        lf = log_formatter('long', to_file=body)
        show_log(self._branch, lf, start_revision=rev1, end_revision=rev2,
                verbose=True, direction='forward')

        msg = EmailMessage(revision.committer, options.email, u'%s r%d: %s' %
                (self.path, rev1, revision.get_summary()), body.getvalue())

        msg['Date'] = format_date(revision.timestamp, revision.timezone,
                date_fmt='%a, %d %b %Y %H:%M:%S')

        if options.line_limit != 0:
            diff = cStringIO.StringIO()
            diff_name = 'r%d.diff' % rev1

            revid_new = revision.revision_id

            self._branch.repository.lock_read()
            try:
                if revision.parent_ids:
                    revid_old = revision.parent_ids[0]
                    tree_new, tree_old = self._branch.repository.revision_trees(
                            (revid_new, revid_old))
                else:
                    # revision_trees() doesn't allow None or 'null:' to be 
passed
                    # as a revision. So we need to call revision_tree() twice.
                    revid_old = _mod_revision.NULL_REVISION
                    tree_new = self._branch.repository.revision_tree(revid_new)
                    tree_old = self._branch.repository.revision_tree(revid_old)
            finally:
                self._branch.repository.unlock()

            show_diff_trees(tree_old, tree_new, diff)
            numlines = diff.getvalue().count('\n') + 1
            if numlines <= options.line_limit or options.line_limit < 0:
                diff = diff.getvalue()
            else:
                diff = ("Diff too large for email (%d lines, the limit is 
%d).\n" %
                        (numlines, options.line_limit))

            msg.add_inline_attachment(diff, diff_name)

        return msg

    def process_IN_CREATE(self, event):
        if event.name in ['revision-history', 'last-revision']:
            self.update()

    process_IN_MODIFY = process_IN_CREATE
    process_IN_MOVED_TO = process_IN_CREATE

###

def find_branches(directories):
    branches = set()
    for toplevel in directories:
        for prefix, subdirs, files in os.walk(toplevel):
            if '.bzr' in subdirs and os.path.isdir(
                    os.path.join(prefix, '.bzr/branch')):
                branches.add(prefix)
                subdirs[:] = [] # no nested branches
    return branches

###

def daemonize(logfile):
    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731
    try:
        pid = os.fork()
        if pid > 0:
            os._exit(0)
    except OSError, e:
        print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
        sys.exit(1)

    os.setsid()
    # os.umask(0022)

    try:
        pid = os.fork()
        if pid > 0:
            os._exit(0)
    except OSError, e:
        print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
        sys.exit(1)

    devnull = os.open('/dev/null', os.O_RDONLY)
    logfile = os.open(logfile, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0660)

    os.dup2(devnull, sys.stdin.fileno())
    os.dup2(logfile, sys.stdout.fileno())
    os.dup2(logfile, sys.stderr.fileno())

    os.close(devnull)
    os.close(logfile)

    # os.chdir('/')

###

def parse_options():
    p = optparse.OptionParser(usage='%prog -e ADDRESS [options] BRANCH [ BRANCH 
... ]')

    p.add_option('-e', '--email', action='store',
            help='email address to send commit mails to')

    p.add_option('-r', '--recurse', action='append', dest='recurse_dirs',
            help=('watch all branches found under DIR; can be specified '
                  'multiple times'), metavar='DIR')

    p.add_option('-l', '--line-limit', action='store', type='int', metavar='N',
            help=('do not attach the diff if bigger than N lines '
                  '(default: 1000; -1: unlimited; 0: no diff)'))

    p.add_option('-d', '--daemon', action='store_true', dest='daemon',
            help='run as a daemon, watching branches with inotify')

    p.add_option('-f', '--foreground', action='store_false', dest='bg_fork',
            help='do not detach from the controlling terminal')

    p.add_option('-o', '--logfile', action='store', metavar='FILE',
            help='print daemon errors to FILE (default: /dev/null)')

    p.set_defaults(bg_fork=True, logfile='/dev/null', line_limit=1000)

    options, args = p.parse_args()

    return options, args

###

if __name__ == '__main__':
    try:
        main()
    except StandardError:
        if options.daemon and options.bg_fork:
            # log the traceback into an specific file if in daemon bg mode
            exception_log = 
os.path.expanduser('~/.bzr_hookless_email.traceback')
            exception_log = os.open(exception_log, os.O_WRONLY | os.O_CREAT, 
0660)
            os.dup2(exception_log, sys.stderr.fileno())
            os.close(exception_log)
        raise



reply via email to

[Prev in Thread] Current Thread [Next in Thread]