Index: CVSROOT/checkoutlist ================================================================== --- CVSROOT/checkoutlist +++ CVSROOT/checkoutlist @@ -9,5 +9,6 @@ # File format: # # [] # # comment lines begin with '#' +syncmail Index: CVSROOT/loginfo ================================================================== --- CVSROOT/loginfo +++ CVSROOT/loginfo @@ -22,5 +22,10 @@ # # For example: #DEFAULT (echo ""; id; echo %s; date; cat) >> $CVSROOT/CVSROOT/commitlog # or #DEFAULT (echo ""; id; echo %{sVv}; date; cat) >> $CVSROOT/CVSROOT/commitlog + +# Lines to mail changes +CVSROOT $CVSROOT/CVSROOT/syncmail %{sVv} mtt@gawthrop.net +DEFAULT $CVSROOT/CVSROOT/syncmail %{sVv} mtt-cvs@lists.sourceforge.net + ADDED CVSROOT/syncmail Index: CVSROOT/syncmail ================================================================== --- /dev/null +++ CVSROOT/syncmail @@ -0,0 +1,287 @@ +#! /usr/bin/python + +# NOTE: Until SourceForge installs a modern version of Python on the cvs +# servers, this script MUST be compatible with Python 1.5.2. + +"""Complicated notification for CVS checkins. + +This script is used to provide email notifications of changes to the CVS +repository. These email changes will include context diffs of the changes. +Really big diffs will be trimmed. + +This script is run from a CVS loginfo file (see $CVSROOT/CVSROOT/loginfo). To +set this up, create a loginfo entry that looks something like this: + + mymodule /path/to/this/script %%s some-email-addr@your.domain + +In this example, whenever a checkin that matches `mymodule' is made, this +script is invoked, which will generate the diff containing email, and send it +to some-email-addr@your.domain. + + Note: This module used to also do repository synchronizations via + rsync-over-ssh, but since the repository has been moved to SourceForge, + this is no longer necessary. The syncing functionality has been ripped + out in the 3.0, which simplifies it considerably. Access the 2.x versions + to refer to this functionality. Because of this, the script is misnamed. + +It no longer makes sense to run this script from the command line. Doing so +will only print out this usage information. + +Usage: + + %(PROGRAM)s [options] <%%S> email-addr [email-addr ...] + +Where options is: + + --cvsroot= + Use as the environment variable CVSROOT. Otherwise this + variable must exist in the environment. + + --help / -h + Print this text. + + --context=# + -C # + Include # lines of context around lines that differ (default: 2). + + -c + Produce a context diff (default). + + -u + Produce a unified diff (smaller). + + --quiet/-q + Don't print as much status to stdout. + + <%%S> + CVS %%s loginfo expansion. When invoked by CVS, this will be a single + string containing the directory the checkin is being made in, relative + to $CVSROOT, followed by the list of files that are changing. If the + %%s in the loginfo file is %%{sVv}, context diffs for each of the + modified files are included in any email messages that are generated. + + email-addrs + At least one email address. +""" +import os +import sys +import time +import string +import getopt +import smtplib +import pwd +import socket + +from cStringIO import StringIO + +# Which SMTP server to do we connect to? Empty string means localhost. +MAILHOST = '' +MAILPORT = 25 + +# Diff trimming stuff +DIFF_HEAD_LINES = 20 +DIFF_TAIL_LINES = 20 +DIFF_TRUNCATE_IF_LARGER = 1000 + +EMPTYSTRING = '' +SPACE = ' ' +DOT = '.' +COMMASPACE = ', ' + +PROGRAM = sys.argv[0] + +BINARY_EXPLANATION_LINES = [ + "(This appears to be a binary file; contents omitted.)\n" + ] + + +def usage(code, msg=''): + print __doc__ % globals() + if msg: + print msg + sys.exit(code) + + + +def calculate_diff(filespec, contextlines): + try: + file, oldrev, newrev = string.split(filespec, ',') + except ValueError: + # No diff to report + return '***** Bogus filespec: %s' % filespec + if oldrev == 'NONE': + try: + if os.path.exists(file): + fp = open(file) + else: + update_cmd = 'cvs -fn update -r %s -p %s' % (newrev, file) + fp = os.popen(update_cmd) + lines = fp.readlines() + fp.close() + # Is this a binary file? Let's look at the first few + # lines to figure it out: + for line in lines[:5]: + for c in string.rstrip(line): + if c in string.whitespace: + continue + if c < ' ' or c > chr(127): + lines = BINARY_EXPLANATION_LINES[:] + break + lines.insert(0, '--- NEW FILE: %s ---\n' % file) + except IOError, e: + lines = ['***** Error reading new file: ', + str(e), '\n***** file: ', file, ' cwd: ', os.getcwd()] + elif newrev == 'NONE': + lines = ['--- %s DELETED ---\n' % file] + else: + # This /has/ to happen in the background, otherwise we'll run into CVS + # lock contention. What a crock. + if contextlines > 0: + difftype = "-C " + str(contextlines) + else: + difftype = "-u" + diffcmd = "/usr/bin/cvs -f diff -kk %s --minimal -r %s -r %s '%s'" % ( + difftype, oldrev, newrev, file) + fp = os.popen(diffcmd) + lines = fp.readlines() + sts = fp.close() + # ignore the error code, it always seems to be 1 :( +## if sts: +## return 'Error code %d occurred during diff\n' % (sts >> 8) + if len(lines) > DIFF_TRUNCATE_IF_LARGER: + removedlines = len(lines) - DIFF_HEAD_LINES - DIFF_TAIL_LINES + del lines[DIFF_HEAD_LINES:-DIFF_TAIL_LINES] + lines.insert(DIFF_HEAD_LINES, + '[...%d lines suppressed...]\n' % removedlines) + return string.join(lines, '') + + + +def getdomain(): + try: + fqdn = socket.getfqdn() + except AttributeError: + # Python 1.5.2 :( + hostname = socket.gethostname() + byaddr = socket.gethostbyaddr(socket.gethostbyname(hostname)) + aliases = byaddr[1] + aliases.insert(0, byaddr[0]) + aliases.insert(0, hostname) + for fqdn in aliases: + if '.' in fqdn: + break + else: + fqdn = 'localhost.localdomain' + parts = string.split(fqdn, DOT) + return string.join(parts[1:], DOT) + + + +def blast_mail(subject, people, filestodiff, contextlines): + # cannot wait for child process or that will cause parent to retain cvs + # lock for too long. Urg! + if not os.fork(): + # in the child + # give up the lock you cvs thang! + time.sleep(2) + # Create the smtp connection to the localhost + conn = smtplib.SMTP() + conn.connect(MAILHOST, MAILPORT) + user = pwd.getpwuid(os.getuid())[0] + domain = getdomain() + author = '%s@%s' % (user, domain) + s = StringIO() + sys.stdout = s + try: + print '''\ +From: %(author)s +To: %(people)s +Subject: %(subject)s +''' % {'author' : author, + 'people' : string.join(people, COMMASPACE), + 'subject': subject, + } + s.write(sys.stdin.read()) + # append the diffs if available + print + for file in filestodiff: + print calculate_diff(file, contextlines) + finally: + sys.stdout = sys.__stdout__ + resp = conn.sendmail(author, people, s.getvalue()) + conn.close() + os._exit(0) + + + +# scan args for options +def main(): + try: + opts, args = getopt.getopt(sys.argv[1:], 'hC:cuq', + ['context=', 'cvsroot=', 'help', 'quiet']) + except getopt.error, msg: + usage(1, msg) + + # parse the options + contextlines = 2 + verbose = 1 + for opt, arg in opts: + if opt in ('-h', '--help'): + usage(0) + elif opt == '--cvsroot': + os.environ['CVSROOT'] = arg + elif opt in ('-C', '--context'): + contextlines = int(arg) + elif opt == '-c': + if contextlines <= 0: + contextlines = 2 + elif opt == '-u': + contextlines = 0 + elif opt in ('-q', '--quiet'): + verbose = 0 + + # What follows is the specification containing the files that were + # modified. The argument actually must be split, with the first component + # containing the directory the checkin is being made in, relative to + # $CVSROOT, followed by the list of files that are changing. + if not args: + usage(1, 'No CVS module specified') + subject = args[0] + specs = string.split(args[0]) + del args[0] + + # The remaining args should be the email addresses + if not args: + usage(1, 'No recipients specified') + + # Now do the mail command + people = args + + if verbose: + print 'Mailing %s...' % string.join(people, COMMASPACE) + + if specs == ['-', 'Imported', 'sources']: + return + if specs[-3:] == ['-', 'New', 'directory']: + del specs[-3:] + elif len(specs) > 2: + L = specs[:2] + for s in specs[2:]: + prev = L[-1] + if string.count(prev, ',') < 2: + L[-1] = "%s %s" % (prev, s) + else: + L.append(s) + specs = L + + if verbose: + print 'Generating notification message...' + blast_mail(subject, people, specs[1:], contextlines) + if verbose: + print 'Generating notification message... done.' + + + +if __name__ == '__main__': + main() + sys.exit(0)