#! /usr/bin/python # -*- Python -*- # # PLEASE DO NOT EDIT THIS FILE IN PLACE IN THE CVS TREE!!! # check out the CVSROOT directory on your local machine, # make the changes there and commit. Otherwise your changes # will be lost. --gafton # - updated by sopwith to add a --require-keyword option that only sends mail if 'keyword' is found in the comment message # # - updated by gafton to allow for --nodiff option. The nodiff option # still sends out messages when something changes in a directory, but # without the diff output. Particularly useful for directories containing # binary files, such as RPM packages. # # - updated by johnsonm to send diffstat output on the new version of # the file if the filename ends in ".patch" -- turn this off with the # option --nodiffstat if you want # # - updated by johnsonm to truncate at 2000 lines instead of 1000 and # to include 200 instead of 20 lines when truncating. These defaults # may be overridden with --headlines= --taillines=, and --truncate. # # - updated by johnsonm to include commands to view full diffs when # truncating diffs # # - updated by johnsonm to include a list of extensions not to # include as diffs or source (graphics) # # $Id: syncmail,v 1.4 2008/01/24 05:22:44 toshio Exp $ """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/syncmail %%s some-email-addr@your.domain In this example, whenever a checkin that matches `mymodule' is made, the syncmail 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's name is misleading. It no longer makes sense to run this script from the command line. Doing so will only print out this usage information. Usage: syncmail [options] <%%S> email-addr [email-addr ...] Where options is: --cvsroot= Use as the environment variable CVSROOT. Otherwise this variable must exist in the environment. --require-keyword=STRING Only send mail if STRING is found in the commit message. --nodiff Don't bother generating the full diff, just report if the files changed or not. --quiet Don't bother attempting to guess if and how files changed. Just report what the cvs server is passing us. --help -h Print this text. <%%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 pwd import string import time import getopt import re import popen2 import signal import email.MIMEMultipart, email.Message, email.Utils, email.Header import smtplib # Diff trimming stuff DIFF_HEAD_LINES = 200 DIFF_TAIL_LINES = 200 DIFF_TRUNCATE_IF_LARGER = 2000 # Operating mode options optNoDiff = 0 optDiffStat = 1 optQuiet = 0 def usage(errcode, msg=''): print __doc__ % globals() if msg: print msg sys.exit(errcode) def calculate_diff(filespec): global optNoDiff diffcmd="" try: file, oldrev, newrev = string.split(filespec, ',') except ValueError: # No diff to report return '***** Not enough context to create diff for file: %s' % filespec if oldrev == 'NONE': try: if optNoDiff: lines = [] else: fp = open(file) lines = fp.readlines() fp.close() lines.insert(0, '--- NEW FILE %s ---\n' % file) except IOError, e: lines = ['***** Error reading new file: ', str(e)] 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. brief = "" if optNoDiff: brief = "--brief" diffcmd = '/usr/bin/cvs -f diff %s -kk -u -N -r %s -r %s %s' % ( brief, 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) # we do want people to be able to view the whole change easily, # even if it is long if diffcmd: lines.insert(0, 'View full diff with command:\n%s\n' % diffcmd) return string.join(lines, '') def create_diffstat(filespec, cvs_dir): lines = "" try: file, oldrev, newrev = string.split(filespec, ',') except ValueError: # No file to diffstat return '***** Not enough context to create diffstat for file: %s' % filespec if newrev == 'NONE': return lines if string.find(file, ".patch", -6) != -1 or \ string.find(file, ".diff", -5) != -1: # run diffstat on the patch if oldrev == 'NONE': # not in CVS yet diffcmd = '/usr/bin/diffstat %s 2>/dev/null' % file else: diffcmd = '/usr/bin/cvs -f co -p -r %s %s/%s 2>/dev/null | /usr/bin/diffstat 2>/dev/null' % ( newrev, cvs_dir, file) fp = os.popen(diffcmd) lines = fp.readlines() fp.close() lines.insert(0, '%s:\n' % file) return string.join(lines, '') # send mail using smtp, with utf8 encoded sender names def blast_mail(subject, people): global optDiffStat cvs_dir = string.split(subject)[0] cvs_module = string.split(cvs_dir, "/")[0] diff_files = string.split(subject)[1:] if diff_files[-3:] == ['-', 'New', 'directory']: del diff_files[-3:] if diff_files[-3:] == ['-', 'Imported', 'sources']: del diff_files[-3:] commit_message = sys.stdin.read() for I in optRequireKeyword: if commit_message.find(I) < 0: return # 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 # Need to wait on CVS to release the lock so we can do the diff msg = email.Message.Message() msg.add_header('Subject', subject) if os.environ.has_key("CVSROOT"): msg.add_header("X-CVSROOT", os.environ["CVSROOT"]) username = pwd.getpwuid(os.getuid())[0] hostname = os.uname()[1] # Get commiter's real-name, and utf8 encode it sender_name = pwd.getpwuid(os.getuid())[4] header_charset = 'utf-8' sender_name = str(email.Header.Header(unicode(sender_name,'utf-8'), header_charset)) sender_email = username + "@fedoraproject.org" sender = email.Utils.formataddr((sender_name, sender_email)) msg.add_header('From', sender) msg.add_header('To', ", ".join(people.split()) ) msg.add_header("X-CVS-Module", cvs_module) msg.add_header("X-CVS-Directory", cvs_dir) msg.add_header("X-CVS-User", username) msg.add_header("X-CVS-Server", hostname) msg.add_header("Precedence", "first-class") payload ="Author: %s\n\n" % (username,) # now insert the CVS blurb payload += commit_message payload += '\n' # append the diffs if available and sensible graphics = re.compile(r".*\.(jp(e)?g|gif|png|tif(f)?),", re.IGNORECASE) for file in diff_files: if optQuiet: payload += ("ChangeSet: %s\n" % (file,)) continue if not graphics.match(file): if optDiffStat: payload += (create_diffstat(file, cvs_dir)) payload += '\n' payload += calculate_diff(file) payload += '\n' msg.set_payload(payload) smtp = smtplib.SMTP("localhost") #smtp.set_debuglevel(1) smtp.sendmail(sender, people.split(), msg.as_string()) smtp.quit() # doesn't matter what code we return, it isn't waited on os._exit(0) # scan args for options def main(): global optNoDiff global optDiffStat global optQuiet global DIFF_HEAD_LINES global DIFF_TAIL_LINES global DIFF_TRUNCATE_IF_LARGER global optRequireKeyword signal.signal(signal.SIGINT, signal.SIG_IGN) try: opts, args = getopt.getopt(sys.argv[1:], 'h', [ 'cvsroot=', 'quiet', 'nodiff', 'nodiffstat', 'help', 'headlines=', 'taillines=', 'truncate=', 'require-keyword=']) except getopt.error, msg: usage(1, msg) optRequireKeyword = [] # parse the options for opt, arg in opts: if opt in ('-h', '--help'): usage(0) elif opt == '--cvsroot': os.environ['CVSROOT'] = arg elif opt == '--quiet': optQuiet = 1 optNoDiff = 0 optDiffStat = 0 elif opt == '--nodiff': optNoDiff = 1 optDiffStat = 0 elif opt == '--nodiffstat': optDiffStat = 0 elif opt == '--headlines': DIFF_HEAD_LINES = arg elif opt == '--taillines': DIFF_TAIL_LINES = arg elif opt == '--truncate': DIFF_TRUNCATE_IF_LARGER = arg elif opt == '--require-keyword': optRequireKeyword.append(arg) # 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] 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 = string.join(args) print 'Mailing %s...' % people blast_mail(subject, people) if __name__ == '__main__': print 'Running syncmail...' main() print '...syncmail done.' sys.exit(0)