#! /usr/bin/python # -*- Python -*- # Copyright (c) 2002, 2003 Barry Warsaw, Fred Drake, and contributors # Copyright (c) 2004 Behdad Esfahbod # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # * Neither the names of the authors nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """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 email-addr [email-addr ...] -- %%{sVv} %%p 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 all provided email addresses. Replacing `mymodule' with `ALL' will... well, if you don't know what it does, you probably don't need this script. This version of the script supports loginfo formats of both CVS versions before 1.12 and after that. Usage: %(PROGRAM)s [options] email-addr [email-addr ...] -- %%{sVv} %%p 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. -u Produce a unified diff (smaller but harder to read, default). --short -s Do not include diffs for modified files in email messages that are generated. email-addrs At least one email address. %%{sVv} %%p CVS loginfo expansion. When invoked by CVS, these will be the the directory the checkin is being made in, relative to $CVSROOT, and the list of files that are changing. The real expansion is a bit different depending the version of CVS server you are running. """ import os import sys import string import time import getopt # Notification command MAILCMD = '/usr/bin/mail -s "%(SUBJECT)s" %(PEOPLE)s 2>&1 > /dev/null' # Diff trimming stuff DIFF_HEAD_LINES = 20 DIFF_TAIL_LINES = 20 DIFF_TRUNCATE_IF_LARGER = 1000 PROGRAM = sys.argv[0] def usage(code, msg=''): print __doc__ % globals() if msg: print msg sys.exit(code) def calculate_diff(filespec, contextlines): try: [file, oldrev, newrev] = filespec except ValueError: # No diff to report return '***** Bogus filespec: %s\n' % `filespec` if oldrev == 'NONE' and newrev == 'NONE': return '' elif 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() 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, '')+'\n' def blast_mail(mailcmd, dodiff, filespecs, 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) fp = os.popen(mailcmd, 'w') fp.write(sys.stdin.read()) fp.write('\n') # append the diffs if available for file in filespecs: fp.write(calculate_diff(file, contextlines)) fp.close() # doesn't matter what code we return, it isn't waited on os._exit(0) # scan args for options def main(): # What follows -- is the specification containing the files that were # modified. The first component containing the directory the checkin # is being made in, relative to $CVSROOT, followed by the list of files # that are changing. args = sys.argv[1:] try: index = args.index("--"); args, specs = args[0:index], args[index+1:] except ValueError: usage(1, "Parameters separator `--' not found") try: opts, args = getopt.getopt(args, 'hC:cus', ['context=', 'cvsroot=', 'help', 'short']) except getopt.error, msg: usage(1, msg) dodiff = True contextlines = 0 # parse the options 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 in ('-s', '--short'): dodiff = False elif opt == '-u': contextlines = 0 # The remaining args should be the email addresses if not args: usage(1, 'No recipients specified') if not specs: usage(1, 'No CVS module specified') if len(specs) <= 2: # Old syntax (before CVS 1.12) # In this syntax, the first parameter contains an string of form: # module file1,oldrev,newrev file2,oldrev,newrev ... if len(specs) > 1: garbage = specs[1] else: garbage = '' specs = specs[0] SUBJECT = specs specs += ' ' index = specs.index(' ') module, specs = specs[:index], specs[index+1:] # Sanity check, are we right? if garbage not in ['', '%p', '%1p', module]: print "Parameters after `--' smell bad. I assumed old style loginfo parameters," print "but seems like I was wrong. Not sure what I'm doing anymore..." # Parse file1,oldrev,newrev file2,oldrev,newrev into filespecs now import re pat = '(.*?),([^, ]*?),([^, ]*?) ' filespecs = re.compile(pat).findall(specs) else: # New syntax (after CVS 1.12) # In this syntax, parameters are of form: # file1 oldrev newrev file2 oldrev newrev ... module filespecs = [] while len(specs) >= 3: filespecs.append(specs[:3]) specs[:3] = [] module = string.join(specs) SUBJECT = module+' '+string.join([','.join(x) for x in filespecs]) # Now do the mail command PEOPLE = string.join(args) mailcmd = MAILCMD % vars() print 'Mailing %s...' % PEOPLE # Filter out non-files from filespecs filespecs = [x for x in filespecs if x[0] not in ["- Imported sources", "- New directory"]] print 'Generating notification message...' blast_mail(mailcmd, dodiff, filespecs, contextlines) print 'Generating notification message... done.' if __name__ == '__main__': main() sys.exit(0)