From: Fred L. D. <fd...@us...> - 2003-07-09 20:49:50
|
Update of /cvsroot/cvs-syncmail/CVSROOT In directory sc8-pr-cvs1:/tmp/cvs-serv6511 Modified Files: syncmail Log Message: eat our own dogfood: update to revision 1.34 Index: syncmail =================================================================== RCS file: /cvsroot/cvs-syncmail/CVSROOT/syncmail,v retrieving revision 1.16 retrieving revision 1.17 diff -u -d -r1.16 -r1.17 --- syncmail 31 Jul 2002 11:51:46 -0000 1.16 +++ syncmail 9 Jul 2003 20:49:42 -0000 1.17 @@ -1,5 +1,9 @@ #! /usr/bin/python +# Copyright (c) 2002, 2003, Barry Warsaw, Fred Drake, and contributors +# All rights reserved. +# See the accompanying LICENSE file for details. + # NOTE: Until SourceForge installs a modern version of Python on the cvs # servers, this script MUST be compatible with Python 1.5.2. @@ -34,8 +38,8 @@ Where options are: --cvsroot=<path> - Use <path> as the environment variable CVSROOT. Otherwise this - variable must exist in the environment. + Use <path> as the environment variable CVSROOT. Otherwise this + variable must exist in the environment. --context=# -C # @@ -44,12 +48,21 @@ -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 - Preprend TEXT to the email subject line. + 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. @@ -57,7 +70,7 @@ --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 @@ -103,12 +116,12 @@ 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 @@ -116,9 +129,6 @@ DIFF_TAIL_LINES = 20 DIFF_TRUNCATE_IF_LARGER = 1000 -EMPTYSTRING = '' -SPACE = ' ' -DOT = '.' COMMASPACE = ', ' PROGRAM = sys.argv[0] @@ -127,8 +137,7 @@ "(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" @@ -141,25 +150,20 @@ -def calculate_diff(filespec, contextlines): - file, oldrev, newrev = string.split(filespec, ',') - # 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 +def calculate_diff(entry, contextlines): + file = entry.name + oldrev = entry.revision + newrev = entry.new_revision - 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 + # Make sure we can find a CVS version number + if oldrev is None and newrev is None: + return NOVERSION % file if string.find(file, "'") <> -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(file, '\\', '\\\\') + filestr = string.replace(filestr, '`', '\`') filestr = string.replace(filestr, '"', '\"') filestr = string.replace(filestr, '$', '\$') # and quote it with double-quotes. @@ -167,7 +171,8 @@ 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) @@ -189,9 +194,10 @@ 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: @@ -202,10 +208,8 @@ % (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] @@ -215,7 +219,17 @@ -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"' % string.replace(name, '"', '\\"') + 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(): @@ -231,23 +245,31 @@ address = '%s@%s' % (user, domain) s = StringIO() sys.stdout = s + datestamp = time.strftime('%a, %d %b %Y %H:%M:%S +0000', + time.gmtime()) try: + vars = {'address' : address, + 'name' : quotename(name), + 'people' : string.join(people, COMMASPACE), + 'subject' : subject, + 'version' : __version__, + 'date' : datestamp, + } + print '''\ +From: %(name)s <%(address)s> +To: %(people)s''' % vars + if replyto: + print 'Reply-To: %s' % replyto print '''\ -From: "%(name)s" <%(address)s> -To: %(people)s Subject: %(subject)s +Date: %(date)s X-Mailer: Python syncmail %(version)s <http://sf.net/projects/cvs-syncmail> -''' % {'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()) @@ -256,12 +278,111 @@ +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 = string.strip(line) + parts = string.split(line, "/") + _, name, revision, timestamp, options, tagdate = parts + key = namekey(prefix, name) + try: + entry = mapping[key] + except KeyError: + if revision == "0": + revision = None + if string.find(timestamp, "+") != -1: + timestamp, conflict = tuple(string.split(timestamp, "+")) + 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 string.strip(line) == "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 = string.split(line, "/")[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 = string.split(line, "/")[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 = string.strip(f.readline()) + 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:cuS:qf:', - ['fromhost=', 'context=', 'cvsroot=', 'subject-prefix=', + 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) @@ -270,6 +391,7 @@ contextlines = 2 verbose = 1 subject_prefix = "" + replyto = None fromhost = None for opt, arg in opts: if opt in ('-h', '--help'): @@ -285,10 +407,15 @@ 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 @@ -296,6 +423,8 @@ # $CVSROOT, followed by the list of files that are changing. if not args: usage(1, 'No CVS module specified') + changes = load_change_info() + branch = load_branch_name() subject = subject_prefix + args[0] specs = string.split(args[0]) del args[0] @@ -307,26 +436,15 @@ # Now do the mail command people = args - if verbose: - print 'Mailing %s...' % string.join(people, COMMASPACE) - 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 if verbose: + print 'Mailing %s...' % string.join(people, COMMASPACE) 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.' |