diff --git a/CVSROOT/syncmail b/CVSROOT/syncmail index 3cdc69c31e..3a75302e43 100755 --- a/CVSROOT/syncmail +++ b/CVSROOT/syncmail @@ -1,7 +1,11 @@ #! /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. +# Copyright (c) 2002, 2003, Barry Warsaw, Fred Drake, and contributors +# All rights reserved. +# See the accompanying LICENSE file for details. + +# NOTE: SourceForge currently runs Python 2.2.3, so we need to remain +# compatible with the Python 2.2 line. """Complicated notification for CVS checkins. @@ -34,8 +38,8 @@ Usage: Where options are: --cvsroot= - Use as the environment variable CVSROOT. Otherwise this - variable must exist in the environment. + Use as the environment variable CVSROOT. Otherwise this + variable must exist in the environment. --context=# -C # @@ -44,16 +48,29 @@ Where options are: -c Produce a context diff (default). + -m hostname + --mailhost hostname + The hostname of an available SMTP server. The default is + 'localhost'. + -u Produce a unified diff (smaller). + -S TEXT + --subject-prefix=TEXT + Prepend TEXT to the email subject line. + + -R ADDR + --reply-to=ADDR + Add a "Reply-To: ADDR" header to the email message. + --quiet / -q Don't print as much status to stdout. --fromhost=hostname -f hostname The hostname that email messages appear to be coming from. The From: - header will of the outgoing message will look like user@hostname. By + header of the outgoing message will look like user@hostname. By default, hostname is the machine's fully qualified domain name. --help / -h @@ -71,40 +88,21 @@ The rest of the command line arguments are: email-addrs At least one email address. """ -__version__ = '1.2' +__version__ = '1.3' import os import sys import re import time -import string import getopt import smtplib import pwd import socket -try: - from socket import getfqdn -except ImportError: - def getfqdn(): - # 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' - return fqdn - - from cStringIO import StringIO -# Which SMTP server to do we connect to? Empty string means localhost. -MAILHOST = '' +# Which SMTP server to do we connect to? +MAILHOST = 'localhost' MAILPORT = 25 # Diff trimming stuff @@ -112,9 +110,6 @@ DIFF_HEAD_LINES = 20 DIFF_TAIL_LINES = 20 DIFF_TRUNCATE_IF_LARGER = 1000 -EMPTYSTRING = '' -SPACE = ' ' -DOT = '.' COMMASPACE = ', ' PROGRAM = sys.argv[0] @@ -123,8 +118,7 @@ BINARY_EXPLANATION_LINES = [ "(This appears to be a binary file; contents omitted.)\n" ] -REVCRE = re.compile("^(NONE|[0-9.]+)$") -NOVERSION = "Couldn't generate diff; no version number found in filespec: %s" +NOVERSION = "Couldn't generate diff; no version number found for file: %s" BACKSLASH = "Couldn't generate diff: backslash in filespec's filename: %s" @@ -137,33 +131,29 @@ def usage(code, msg=''): -def calculate_diff(filespec, contextlines): - file, oldrev, newrev = string.split(filespec, ',') +def calculate_diff(entry, contextlines): + file = entry.name + oldrev = entry.revision + newrev = entry.new_revision + # Make sure we can find a CVS version number - if not REVCRE.match(oldrev): - return NOVERSION % filespec - if not REVCRE.match(newrev): - return NOVERSION % filespec + if oldrev is None and newrev is None: + return NOVERSION % file - if string.find(file, '\\') <> -1: - # I'm sorry, a file name that contains a backslash is just too much. - # XXX if someone wants to figure out how to escape the backslashes in - # a safe way to allow filenames containing backslashes, this is the - # place to do it. --Zooko 2002-03-17 - return BACKSLASH % filespec - - if string.find(file, "'") <> -1: + if file.find("'") <> -1: # Those crazy users put single-quotes in their file names! Now we # have to escape everything that is meaningful inside double-quotes. - filestr = string.replace(file, '`', '\`') - filestr = string.replace(filestr, '"', '\"') - filestr = string.replace(filestr, '$', '\$') + filestr = filestr.replace('\\', '\\\\') + filestr = filestr.replace('`', '\`') + filestr = filestr.replace('"', '\"') + filestr = filestr.replace('$', '\$') # and quote it with double-quotes. filestr = '"' + filestr + '"' else: # quote it with single-quotes. filestr = "'" + file + "'" - if oldrev == 'NONE': + if oldrev is None: + # File is being added. try: if os.path.exists(file): fp = open(file) @@ -175,8 +165,8 @@ def calculate_diff(filespec, contextlines): # 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: + for c in line.rstrip(): + if c.isspace(): continue if c < ' ' or c > chr(127): lines = BINARY_EXPLANATION_LINES[:] @@ -185,9 +175,10 @@ def calculate_diff(filespec, contextlines): except IOError, e: lines = ['***** Error reading new file: ', str(e), '\n***** file: ', file, ' cwd: ', os.getcwd()] - elif newrev == 'NONE': + elif newrev is None: lines = ['--- %s DELETED ---\n' % file] else: + # File has been changed. # This /has/ to happen in the background, otherwise we'll run into CVS # lock contention. What a crock. if contextlines > 0: @@ -198,20 +189,28 @@ def calculate_diff(filespec, contextlines): % (difftype, oldrev, newrev, filestr) 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) + fp.close() 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, '') + return ''.join(lines) -def blast_mail(subject, people, filestodiff, contextlines, fromhost): +rfc822_specials_re = re.compile(r'[\(\)\<\>\@\,\;\:\\\"\.\[\]]') + +def quotename(name): + if name and rfc822_specials_re.search(name): + return '"%s"' % name.replace('"', '\\"') + else: + return name + + + +def blast_mail(subject, people, entries, contextlines, fromhost, replyto): # cannot wait for child process or that will cause parent to retain cvs # lock for too long. Urg! if not os.fork(): @@ -221,29 +220,38 @@ def blast_mail(subject, people, filestodiff, contextlines, fromhost): # Create the smtp connection to the localhost conn = smtplib.SMTP() conn.connect(MAILHOST, MAILPORT) - user = pwd.getpwuid(os.getuid())[0] - name = pwd.getpwuid(os.getuid())[4] - domain = fromhost or getfqdn() + pwinfo = pwd.getpwuid(os.getuid()) + user = pwinfo[0] + name = pwinfo[4].split(',')[0] + domain = fromhost or socket.getfqdn() address = '%s@%s' % (user, domain) s = StringIO() sys.stdout = s + datestamp = time.strftime('%a, %d %b %Y %H:%M:%S +0000', + time.gmtime(time.time())) try: + vars = {'address' : address, + 'name' : quotename(name), + 'people' : COMMASPACE.join(people), + 'subject' : subject, + 'version' : __version__, + 'date' : datestamp, + } print '''\ From: %(name)s <%(address)s> -To: %(people)s +To: %(people)s''' % vars + if replyto: + print 'Reply-To: %s' % replyto + print '''\ Subject: %(subject)s +Date: %(date)s X-Mailer: Python syncmail %(version)s -''' % {'address' : address, - 'name' : name, - 'people' : string.join(people, COMMASPACE), - 'subject' : subject, - 'version' : __version__, - } +''' % vars s.write(sys.stdin.read()) # append the diffs if available print - for file in filestodiff: - print calculate_diff(file, contextlines) + for entry in entries: + print calculate_diff(entry, contextlines) finally: sys.stdout = sys.__stdout__ resp = conn.sendmail(address, people, s.getvalue()) @@ -252,18 +260,120 @@ X-Mailer: Python syncmail %(version)s +class CVSEntry: + def __init__(self, name, revision, timestamp, conflict, options, tagdate): + self.name = name + self.revision = revision + self.timestamp = timestamp + self.conflict = conflict + self.options = options + self.tagdate = tagdate + +def get_entry(prefix, mapping, line, filename): + line = line.strip() + parts = line.split("/") + _, name, revision, timestamp, options, tagdate = parts + key = namekey(prefix, name) + try: + entry = mapping[key] + except KeyError: + if revision == "0": + revision = None + if timestamp.find("+") != -1: + timestamp, conflict = tuple(timestamp.split("+")) + else: + conflict = None + entry = CVSEntry(key, revision, timestamp, conflict, + options, tagdate) + mapping[key] = entry + return entry + +def namekey(prefix, name): + if prefix: + return os.path.join(prefix, name) + else: + return name + +def load_change_info(prefix=None): + if prefix is not None: + entries_fn = os.path.join(prefix, "CVS", "Entries") + else: + entries_fn = os.path.join("CVS", "Entries") + entries_log_fn = entries_fn + ".Log" + mapping = {} + f = open(entries_fn) + while 1: + line = f.readline() + if not line: + break +## if line.strip() == "D": +## continue + # we could recurse down subdirs, except the Entries.Log files + # we need haven't been written to the subdirs yet, so it + # doesn't do us any good +## if line[0] == "D": +## name = line.split("/")[1] +## dirname = namekey(prefix, name) +## if os.path.isdir(dirname): +## m = load_change_info(dirname) +## mapping.update(m) + if line[0] == "/": + # normal file + get_entry(prefix, mapping, line, entries_fn) + # else: bogus Entries line + f.close() + if os.path.isfile(entries_log_fn): + f = open(entries_log_fn) + while 1: + line = f.readline() + if not line: + break + if line[1:2] != ' ': + # really old version of CVS + break + entry = get_entry(prefix, mapping, line[2:], entries_log_fn) + parts = line.split("/")[1:] + if line[0] == "A": + # adding a file + entry.new_revision = parts[1] + elif line[0] == "R": + # removing a file + entry.new_revision = None + f.close() + for entry in mapping.values(): + if not hasattr(entry, "new_revision"): + print 'confused about file', entry.name, '-- ignoring' + del mapping[entry.name] + return mapping + +def load_branch_name(): + tag_fn = os.path.join("CVS", "Tag") + if os.path.isfile(tag_fn): + f = open(tag_fn) + line = f.readline().strip() + f.close() + if line[:1] == "T": + return line[1:] + return None + # scan args for options def main(): + # XXX Should really move all the options to an object, just to + # avoid threading so many positional args through everything. try: opts, args = getopt.getopt( - sys.argv[1:], 'hC:cuqf:', - ['fromhost=', 'context=', 'cvsroot=', 'help', 'quiet']) + sys.argv[1:], 'hC:cuS:R:qf:m:', + ['fromhost=', 'context=', 'cvsroot=', 'mailhost=', + 'subject-prefix=', 'reply-to=', + 'help', 'quiet']) except getopt.error, msg: usage(1, msg) # parse the options contextlines = 2 verbose = 1 + subject_prefix = "" + replyto = None fromhost = None for opt, arg in opts: if opt in ('-h', '--help'): @@ -277,10 +387,17 @@ def main(): contextlines = 2 elif opt == '-u': contextlines = 0 + elif opt in ('-S', '--subject-prefix'): + subject_prefix = arg + elif opt in ('-R', '--reply-to'): + replyto = arg elif opt in ('-q', '--quiet'): verbose = 0 elif opt in ('-f', '--fromhost'): fromhost = arg + elif opt in ('-m', '--mailhost'): + global MAILHOST + MAILHOST = arg # What follows is the specification containing the files that were # modified. The argument actually must be split, with the first component @@ -288,8 +405,8 @@ def main(): # $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]) + subject = subject_prefix + args[0] + specs = args[0].split() del args[0] # The remaining args should be the email addresses @@ -299,26 +416,18 @@ def main(): # Now do the mail command people = args - if verbose: - print 'Mailing %s...' % string.join(people, COMMASPACE) - - if specs == ['-', 'Imported', 'sources']: + if specs[-3:] == ['-', 'Imported', 'sources']: + print 'Not sending email for 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 + + branch = load_branch_name() + changes = load_change_info() if verbose: + print 'Mailing %s...' % COMMASPACE.join(people) print 'Generating notification message...' - blast_mail(subject, people, specs[1:], contextlines, fromhost) + blast_mail(subject, people, changes.values(), + contextlines, fromhost, replyto) if verbose: print 'Generating notification message... done.'