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 commit-g...@gnu.org /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ó (d...@net.com.org.es) # 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