Index: CVSROOT/adjustlinks.py =================================================================== RCS file: /usr/local/cvsroot/CVSROOT/adjustlinks.py,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ CVSROOT/adjustlinks.py 9 Feb 2004 13:13:44 -0000 1.1 @@ -0,0 +1,316 @@ +#!/usr/bin/python + +"""Adjust symlinks in the repository according to the repolinks recipe file. + +The repolinks file is found in the CVSROOT of the repository, along with this +script. + +The links asserted by repolinks are compared with existing links in the +filesystem, and links are added and deleted as necessary. See +LinkManager.assert_map() for specifics. + +Errors - illegal or invalid paths, collisions, etc - are reported to +stderr.""" + +# Name of the recipe file - to be found in same directory as the script: +RECIPE_NAME = 'repolinks' +QUIET = 0 + +import sys, os, stat +import string +SCRIPT_DIR = os.path.abspath(os.path.split(sys.argv[0])[0]) +# Assume the repository root dir contains the CVSROOT script dir. +REPO_ROOT = os.path.split(SCRIPT_DIR)[0] +RECIPE_PATH = os.path.join(SCRIPT_DIR, RECIPE_NAME) + +EMPTY_DIR = REPO_ROOT + "/CVSROOT/Emptydir" + +def main(argv=None): + lm = LinkManager() + lm.assess_existing_links() + lm.assert_map() + +class LinkManager: + """Maintain and implement repository symlink maps.""" + + def __init__(self): + """Assemble data structures.""" + # [(from, to, lineno), ...] + self._recipe_lines = self.get_recipe_lines() + # {actual_path: actual_target} + self._fslinks = {} + + def assert_map(self, + isdir=os.path.isdir, isfile=os.path.isfile, + islink=os.path.islink, exists=os.path.exists, + abspath=os.path.abspath, split=os.path.split, + readlink=os.readlink, unlink=os.unlink, pjoin=os.path.join, + symlink=os.symlink, lstat=os.lstat, ST_INO=stat.ST_INO): + """Impose the links specified by the recipe, removing preexisting + links that are not specified in the map. Specifically: + + - Add prescribed links that are absent, when there's no non-link + file in the way. + + - Adjust already-existing links that don't point to the prescribed + place - so we can use the file to redirect existing links. + + - Remove links that are not prescribed. + + - Do nothing for links that already exist as prescribed. + + - Warn about prescribed links that point nowhere. + + - Warn about prescribed links that cannot be created because the + indicated containing dir does not exist. + + - Reiterate attempts until all are done, or no more progress is + being made - so it's not a problem for links to be dependent on + others in order to be situated. + + - Delete prescribed links that point outside the repository. + + - Produce output about all the actions.""" + + link2inode = self._fslinks + inode2link = {} + for k, v in link2inode.items(): inode2link[v] = k + + # Confirm or create the prescribed links: + + pending = self._recipe_lines + # Collect accounted-for links inodes, to prevent deletion in cleanup: + done = [] + retargeted = [] + resolvedsome = 1 + + # Since links can depend on eachother, we repeatedly loop as long + # as stuff is pending and we're making progress: + while resolvedsome and pending: + + resolvedsome = 0 + doing = pending + pending = [] + for i in doing: + link, target, lineno = i + + # Is it already there, as a link? + if islink(link): + resolvedsome = 1 + oldtarget = readlink(link) + # Is the existing one pointing at the right place? + if oldtarget != target: + info("Retargeting existing link %s\n" + " => %s (was %s)", + link, target, oldtarget) + retargeted.append(lstat(link)[ST_INO]) + unlink(link) + symlink(target, link) + done.append((link, target, lstat(link)[ST_INO])) + + # Is there a non-link in the way? + elif exists(link): + resolvedsome = 1 + warn("%s line %d: link blocked by non-link %s", + RECIPE_NAME, lineno, link) + + # Is the directory containing it there, to allow creating it? + elif isdir(split(link)[0]): + info("Creating link:\n %s => %s", link, target) + symlink(target, link) + resolvedsome = 1 + done.append((link, target, lstat(link)[ST_INO])) + + # Try again next time, if we're making progress - the + # directory may eventually be established by another link: + else: + pending.append(i) + + # Warn about infeasible links: + for link, target, lineno in pending: + warn("%s line %d: failed to place link %s", + RECIPE_NAME, lineno, link) + + # Cleanup: + # - Remove any links that point outside the repository hierarchy + # - Warn about links that don't point anywhere valid + # - Remove obsolete links + + rootlen = len(REPO_ROOT) + for link, target, inode in done: + + # Remove links that point outside the repository hierarchy: + if abspath(pjoin(link, target))[:rootlen] != REPO_ROOT: + warn("Removing illegal link to outside repository:\n" + " %s => %s", + link, target) + unlink(link) + + elif not exists(link): + warn("Orphaned link - points at nothing:\n %s => %s", + link, readlink(link)) + + # Cull accounted-for inode2link links, preparing for next step. + if inode2link.has_key(inode): + del inode2link[inode] + + # Most links still registered in inode2link are obsolete - remove them: + for inode, link in inode2link.items(): + if inode not in retargeted: + info("Removing obsolete link %s", link) + unlink(link) + + def get_recipe_lines(self, + strip=string.strip, split=string.split, + pjoin=os.path.join, abspath=os.path.abspath, + isabs=os.path.isabs): + """Return massaged list of recipe lines, as tuples: + (from, to, lineno). We omit non-recipe lines, and warn about + malformed ones. + + See comments at repolinks file top for format.""" + lines = open(RECIPE_PATH).readlines() + lineno = 0 + got = [] + for i in lines: + lineno = lineno + 1 + i = strip(i) + if not i or i[0] == '#': + continue + fields = split(i) + if len(fields) > 2: + warn("Skipping bad line %d - %d fields, should not exceed 2", + lineno, len(fields)) + continue + elif len(fields) == 1: + # Empty target means point to EMPTY_DIR - place holder for + # removed dirs. + link, target = fields[0], EMPTY_DIR + else: + link, target = fields + if isabs(target): + target = REPO_ROOT + target + delim = (not isabs(link) and os.sep) or '' + link = REPO_ROOT + delim + link + got.append((link, target, lineno)) + # Warn about dups: + links = {} + for link, target, lineno in got: + if links.has_key(link): + warn("Duplicate link encountered on line %d (last on %d):\n" + " %s", lineno, links[link], link) + links[link] = lineno + return got + + def assess_existing_links(self, readlink=os.readlink, lstat=os.lstat, + ST_INO=stat.ST_INO): + """Walk the repository from the root, collecting existing links.""" + + m = {} + for i in find_fs_links(REPO_ROOT): + m[i] = lstat(i)[ST_INO] + self._fslinks = m + return m + + def all_containers(self, path, + exists=os.path.exists, ST_INO=stat.ST_INO): + """Given a repo-relative path, return *all* the repository paths that + contain it - direct containers *and* directories that container it by + virtue of symlinks in the repolinks map. + + This is used in postcommit_actions to identify all the + checkin-notification subscriber entries that qualify for a particular + file, not just the containers obvious from the checkin path. + + We expect a path relative to the repository root, and return paths + relative to the repository root.""" + # We get the inodes of all the directories on the actual (physical) + # path, and identify all those links whose targets are one of those + # directories. + + got = {path: 1} + + if path[:len(os.sep)] == os.sep: + # Strip the leading '/' + path = path[len(os.sep):] + path = os.path.join(REPO_ROOT, path) + + actual_path, element_inodes = path_element_inodes(path) + if element_inodes: + # Include actual path (relative to actual repo root). + got[actual_path[len(ACTUAL_REPO_ROOT())+len(os.sep):]] = 1 + for link, target, lineno in self._recipe_lines: + if (exists(target) + and (os.stat(target)[ST_INO] in element_inodes)): + # (Strip the REPO_ROOT prefix.) + got[link[len(REPO_ROOT)+len(os.sep):]] = 1 + + return got.keys() + +def path_element_inodes(path, split=os.path.split, + stat=os.stat, ST_INO=stat.ST_INO): + """Return actual path relative to repository, and root inodes of directory + elements leading to path's physical location. + We return the empty list if path doesn't actually exist.""" + + actual_path = None + got = [] + if (os.path.exists(path) or os.path.exists(split(path)[0])): + here = actual_path = actual_dir(path) + while here and (here != '/'): + got.insert(0, stat(here)[ST_INO]) + here = split(here)[0] + return (actual_path, got) + +def find_fs_links(dir, + join=os.path.join, + isdir=os.path.isdir, islink=os.path.islink): + """Return a breadth-first list of paths of symlinks within dir.""" + got = [] + dirs = [] + + for f in os.listdir(dir): + p = join(dir, f) + if islink(p): got.append(p) + elif isdir(p): dirs.append(p) + + for d in dirs: + got.extend(find_fs_links(d)) + + return got + +def actual_dir(path): + """Return the real directory, as reported by os.getcwd from inside the dir. + + If the arg is not a directory, try with the final path element stripped.""" + origdir = os.getcwd() + try: + if os.path.isdir(path): + os.chdir(path) + else: + os.chdir(os.path.split(path)[0]) + return os.getcwd() + + finally: + os.chdir(origdir) + +_ACTUAL_REPO_ROOT = None +def ACTUAL_REPO_ROOT(): + global _ACTUAL_REPO_ROOT + if _ACTUAL_REPO_ROOT is None: + _ACTUAL_REPO_ROOT = actual_dir(REPO_ROOT) + return _ACTUAL_REPO_ROOT + +def info(*args): + if not QUIET: + apply(warn, args, {'status': "-- Info"}) + +def warn(*args, **kw): + status = kw.get('status', "** Warning") + print "%s: %s" % (status, args[0] % args[1:]) + +if __name__ == "__main__": + print (" === %s run ===\n ...on commit of %s...\n" + % (sys.argv[0], RECIPE_PATH)) + main(sys.argv) + print "-- Done." Index: CVSROOT/postcommit_actions =================================================================== RCS file: /usr/local/cvsroot/CVSROOT/Attic/postcommit_actions,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ CVSROOT/postcommit_actions 9 Feb 2004 13:13:44 -0000 1.1 @@ -0,0 +1,636 @@ +#!/usr/bin/python + +# Adapted by bdolicki for openacs.org +# Changes to http://dev.zope.org/CVS/postcommit_actions : +# 001 +# The original file forced python2.1 because os.popen2 didn't work for them. +# on Python 1.5. It does work for us though so we didn't install python2.1 +# 002 +# Set some variables + +"""Apply checkin actions dictated by traffic_table.py, as checkins occur. + +Special provisions to fork into the background, disconnected from tty, so +the user and (more critically) CVS locks aren't held just pending completion +of notification and other potentially meandering actions. + +Options: + + --verbose - operate in verbose mode, with output to OUTPUT_LOG in the + detached copy, and to stdout before the detach (and forever if + --wait specified) + + --dryrun - do not actually do any of the activities + + --wait - do not fork into the background (and leave output in stdout/stderr) + This is for internal use by the script when forking. + + --msgfile MSGFILENM - internal - get log message from MSGFILENM (and delete) + +We expect to be invoked from loginfo with %{CVSROOT} %{sVv}: CVS expands +the loginfo %{sVv} into a single command-line token (bizarrely enough) +containing multiple, space-delimited parts: + + - The first part is the path of repository directory + + - Then comes a comma-concatenated string for each file being checked in, + consisting of: + + - The file name, + - The old version number, then + - the new one, like so: + + filename,1.52,1.53 + +\(See the CVS loginfo docs for more format-string details.) + +The actions are configured in traffic_table.py. The only remaining relevant +one is: + + - email checkin messages to designated recipients + +\(Versions of the script that supported automatic mirroring was removed +beginning at version 1.123.)""" + +import sys, os, tempfile, time, pwd +SCRIPT_DIR = os.path.abspath(os.path.split(sys.argv[0])[0]) +os.chdir(SCRIPT_DIR) # Valid dir, having other local scripts. +import string +import getopt +import smtplib +import socket, whrandom, getpass + +CVSROOT = os.path.split(SCRIPT_DIR)[0] +CVSROOT_ABS = os.path.abspath('..') +# 002 +#USE_LDAP = 1 +USE_LDAP = 0 +LDAP_PORT = 389 +LDAP_HOST = "your.ldap.host" +# 002 TODO is it OK to use this address? +#OFFICIAL_SENDER = "cvs-admin@your.domain" +OFFICIAL_SENDER = "openacs-cvs-list-admin@willfork.com" + +# Ceiling on number of lines per file report (diff or "added"), at which point +# we go to excerpts from the beginning and end: +FILE_LINES_LIMIT = 1500 +# Number of lines in beginning and end excerpts when FILE_LINES_LIMIT is hit: +FILE_EXCERPTS_LINES = 200 + +CUSTOM_TRAFFIC_TABLE = "%s/custom_traffic_table.py" % CVSROOT + +import re + +import traffic_table +import adjustlinks + +# Hook up with an outboard file. +if os.path.exists(CUSTOM_TRAFFIC_TABLE): + execfile(CUSTOM_TRAFFIC_TABLE) + +# OUTPUT_LOG must be writable by anyone whose checkins will invoke this +# script in order for their logging to happen. +OUTPUT_LOG = "%s/cvs-postcommit_actions.log" % CVSROOT + +# Set CVSMASTER to the email address of someone managing your CVS mirroring. +# Notices about any caught malfunctions will be sent to this address. +# 002 TODO is it OK to use this address? +#CVSMASTER = "cvs-admin@zope.com" +CVSMASTER = "openacs-cvs-list-admin@willfork.com" + +# You have to plug in the recipients email address via '%' string formatting. +MAIL_CMD = "/usr/lib/sendmail -t -f %s" + +WAIT = 0 # Fork unless --wait is requested. +DRYRUN = 0 # '--dryrun' option +VERBOSE = 0 # '--verbose' option default value +SCRIPT = "postcommit_actions" # Will b set to argv[0], when obtained. + +sVv_re = re.compile("(.*),([^,]+),([^,]+)") + +def main(args): + """Grok the args and the traffic_table and process accordingly.""" + + global SCRIPT, DRYRUN, VERBOSE, WAIT + + orig_args = args[:] + + SCRIPT = args[0] + try: + opts, args = getopt.getopt(args[1:], "", ["verbose", "dryrun", + "wait", "msgfile=", + ]) + except getopt.error, err: + complain("%s\n", err) + raise SystemExit, 1 + + msgfilenm = "" + + for opt, val in opts: + if opt == "--verbose": + VERBOSE = 1 + complain("%s: VERBOSE\n", SCRIPT) + elif opt == "--dryrun": + DRYRUN = 1 + complain("%s: DRYRUN\n", SCRIPT) + elif opt == "--wait": + WAIT = 1 + complain("%s: WAIT/Foreground\n", SCRIPT) + elif opt == "--msgfile": + # This is internal, for the script to pass itself a file. + msgfilenm = val + else: + complain("Unrecognized option '%s'\n", opt) + raise SystemExit, 1 + if VERBOSE: + complain("Initial args: %s '%s'\n", + string.join(orig_args[:-1], " "), + orig_args[-1]) + complain("Working dir: %s\n" % safe_getcwd('/tmp')) + + if len(args) != 1: + usage(); raise SystemExit, 1 + + doing_directory = 0 + no_files = 1 + + # The %{sVv} args are passed in as a single token - have to split them. + split_args = string.split(args[0]) + repo = split_args[0] + + if len(split_args) == 0: + subjs = ["", "", ""] + elif len(split_args) == 1: + subjs = split_args + ["", ""] + elif split_args[1] == "-": + subjs = split_args + doing_directory = 1 + else: + no_files = 0 + subjs = map(grok_file, split_args[1:]) + + if VERBOSE: + complain("CWD: %s, Repo: %s, ", + safe_getcwd('/tmp'), repo) + complain("Subjects: %s\n", subjs) + + if not WAIT: + detach(orig_args) + + entries = find_entries(repo) + + for entry in entries: + wasVerbose = VERBOSE + VERBOSE = entry.get('verbose', VERBOSE) + selector_path = entry['path'] + addrs = entry.get('addrs') + specials = entry.get('specials') + + if addrs: + do_mail(repo, addrs, subjs, msgfilenm=msgfilenm, + doing_directory=doing_directory, no_files=no_files) + + if specials: + subj_names = map(lambda x: x[0], subjs) + for trigger, action in specials: + if trigger in subj_names: + do_special(trigger, action, addrs) + + VERBOSE = wasVerbose + + if VERBOSE: + complain("** Done **\n\n") + + if failures: + handle_failures(orig_args) + + if msgfilenm: + os.unlink(msgfilenm) + +def detach(args): + """Fork us into the background, with stdout and stderr to OUTPUT_LOG. + + We have to disconnect std io (in, out, and err) and run the program in + the background - which we do using some shell wizardry.""" + + doctored_args = [args[0]] + doctored_args.append("--wait") + tempfile.mktemp() # Throw one away, to get initial template. + tempfile.tempdir = "/tmp" +## template = (tempfile.template or +## "@%s.%d" % (os.getpid(), whrandom.randint(1000000, 9999999))) +## tempfile.template = "cvs-log" + template + msgfile = open(tempfile.mktemp(suffix='.cvslog'), 'w') + msgfile.write(sys.stdin.read()) + msgfile.close() + msgfilenm = msgfile.name + doctored_args.append("--msgfile") + doctored_args.append(msgfilenm) + for i in args[1:]: + doctored_args.append('"%s"' % i) + cmd = (("( exec 1>>%s 2>&1; " + + ("export CVSROOT=%s; " % CVSROOT) + + string.join(doctored_args, " ") + " &)") + % OUTPUT_LOG) + + if VERBOSE: + complain("Re-executing detached in %s, cmd:\n\t%s\n", + safe_getcwd('/tmp'), cmd) + + os.system(cmd) + loosen_file(OUTPUT_LOG) + + os._exit(0) + +def find_entries(repo=None): + """Return dictionary of traffic_table entries qualified by repo regexp. + + Iff no entries match, we return the catchall entry - the (last) one with + path == None.""" + + entries = [] + catchall = None + linkmgr = adjustlinks.LinkManager() + gotaddrs = [] + + containers = linkmgr.all_containers(repo) + for it in traffic_table.get_table(): + if it['path'] == None: + # Retain the catchall entry in case no regular ones qualify. + catchall = it + else: + # Obtain qualifying candidates: + for candidate in containers: + + if (re.match(it['path'], candidate) + and it.get('addrs') not in gotaddrs): + entries.append(it) + gotaddrs.append(it.get('addrs')) + + if entries: + if VERBOSE > 1: + complain("find_entries: repo: %s, containers: %s\n entries: %s\n", + repo, containers, entries) + elif catchall: + entries.append(catchall) + if VERBOSE > 1: + complain("No matches, so using catchall:\n %s\n", entries) + elif VERBOSE > 1: + complain("No matches, no catchall - no actions\n") + + for e in entries: + if e.has_key('addrs') and (type(e['addrs']) == type("")): + # Be lenient - listify string args. + e['addrs'] = [e['addrs']] + + return entries + +def do_mail(repo, addrs, subjs, + msgfilenm, doing_directory=0, no_files=0): + """Send notice about checkin to addresses dictated by traffic table. + + We include a diff.""" + if VERBOSE: + complain("Notice to %s\n for %s / %s\n", addrs, repo, subjs) + # The message contents are on stdin, just _yearning_ to be sent...-) + subject = "CVS: %s " % repo + + diff_msg = '' + + if doing_directory or no_files: + subject = subject + string.join(subjs, " ") + if subjs[2] == 'New': + new_msg = ('=== Added directory %s ===\n' % repo) + diff_msg = diff_msg + new_msg + '\n' + else: + subject = subject + "-" + for fn, old, new in subjs: + subject = subject + " %s:%s" % (fn, new) + if new == 'NONE': + new_msg = ('=== Removed File %s/%s ===\n' + % (repo, fn)) + else: + try: + new_msg = "\n" + create_diff(repo, fn, old, new) + except IOError: + if DRYRUN: + text = "[Contrived diff]" + else: + raise + + + diff_msg = diff_msg + new_msg + '\n' + + try: + # Prepend the Subject and From lines, and append the diff: + mf = open(msgfilenm, 'r') + text = mf.read() + diff_msg + mf.close() + except IOError: + if DRYRUN and WAIT: + text = "[Contrived content]\n" + diff_msg + else: + raise + + send_mail(addrs, text, subject) + +def create_diff(repo, file, old, new): + """ Create a diff comparing old and new versions """ + if old == 'NONE': # A new file was added + # We have to change to a neutral dir, or cvs will complain about + # doing checkouts into the repository - even though the checkout + # is to stdout, sigh. + origdir = os.getcwd() + try: + os.chdir('/tmp') + + revclause = '' + if new and new != 'NONE': + revclause = '-r ' + new + + # "Checkout" to stdout, so we can collect the lines to return. + co_stdout_cmd = 'cvs -fn co -p %s %s/%s' % (revclause, repo, file) + handle = os.popen(co_stdout_cmd) + lines = handle.readlines() + handle.close() + header = ("=== Added File %s/%s ===" + % (repo, file)) + + finally: + os.chdir(origdir) + + else: # A "normal" update happened + diff_cmd = ('cvs -d %s -f rdiff -r %s -r %s -kk -u %s/%s' + % (CVSROOT_ABS, old, new, repo, file)) + file_handle = os.popen(diff_cmd) + lines = file_handle.readlines()[2:] + file_handle.close() + header = ("=== %s/%s %s => %s ===" + % (str(repo), str(file), old, new)) + + # Provide for initial checkins on branch - in which case, the ,v files + # exist only in the Attic. + if (old == 'NONE') and (len(new.split('.')) > 2): + template = "%s/%s/Attic/%s,v" + else: + template = "%s/%s/%s,v" + commav = template % (CVSROOT_ABS, repo, file) + if os.path.exists(commav): + isbinary = expands_as_binary(commav) + else: + complain("Couldn't check binary-ness of missing comma-v" + " for %s%s v %s, should be at:\n %s" + % (((old == 'NONE') and "new ") or '', + file, new, commav)) + # Let the diff go ahead: + isbinary = None + + if isbinary: + return "%s\n " % header + else: + total = len(lines) + if total >= FILE_LINES_LIMIT: + omitted = total - (2 * FILE_EXCERPTS_LINES) + # Limit exceeded, show only exercpts from beginning and end. + lines = (lines[:FILE_EXCERPTS_LINES] + + ['\n', + ('[-=- -=- -=- %s lines omitted -=- -=- -=-]\n' + % (total - (2 * FILE_EXCERPTS_LINES))), + '\n'] + + lines[-FILE_EXCERPTS_LINES:]) + header = header + " (%s/%s lines abridged)" % (omitted, total) + lines.insert(0, header + "\n") + return string.join(lines, '') + +def do_special(trigger, action, addrs): + """Do special action - a script, to be invoked from the CVSROOT dir. + + Run it with the version info and send the result to indicated addrs.""" + + action_cmd = "%s/CVSROOT/%s %s" % (CVSROOT, action, trigger) + file_handle = os.popen(action_cmd) + + output = file_handle.read() + result = file_handle.close() + + if result: + note_failure("*** Special command failed with error %s:\n" + "** %s **\n%s\n", + result, action_cmd, output) + else: + subject = "CVS: %s run for %s changes" % (action, trigger) + send_mail(addrs, output, subject) + complain("%s\n%s\n", subject, output) + +def send_mail(addrs, text, subject): + user = getuser() + fullname = email = '' + if user: + fullname, email = get_user_ldap_info(user) + if '.' in fullname or ',' in fullname: + fullname = '"' + fullname + '"' + else: + user = "*unknown*" + if not fullname: + fullname = "The Unidentified User" + if not email: + email = OFFICIAL_SENDER + + cmd_info = {'verbose1': (VERBOSE and "set -x; ") or "", + 'dryrun': (DRYRUN and "echo Would do: ") or "", + 'mailcmd': MAIL_CMD % email, + 'verbose2': (VERBOSE and "-v") or ""} + cmd = ("%(verbose1)s%(dryrun)s%(mailcmd)s %(verbose2)s" % cmd_info) + + if VERBOSE: + complain("%sDoing mail cmd for user %s:\n\t%s\n", + ((DRYRUN and "NOT ") or ""), user, cmd) + + envelope_info = {'subject': subject, + 'to': string.join(addrs, ", "), + 'from': "%s <%s>" % (fullname, email), + 'sender': OFFICIAL_SENDER, + 'user': user} + header = ("Subject: %(subject)s\n" + "To: %(to)s\n" + "From: %(from)s\n" + "Sender: %(sender)s\n" + % envelope_info) + + notice = header + '\n' + text + + if not DRYRUN: + cmd_in, cmd_out = os.popen2(cmd, 'rw') + cmd_in.write(notice) + cmd_in.close() + output = cmd_out.read() + result = cmd_out.close() + else: + result = None + + if VERBOSE: + complain(string.join(map(lambda x: '= ' + x, + string.split(header, '\n')), + '\n')) + if result: + note_failure("*** Mail cmd yielded unexpected result %s:\n%s\n", + result, output) + +def loosen_file(fname): + """Relax permissions on (newly created) file so others can use it too.""" + try: + os.chmod(fname, 0777) + except os.error, err: + pass + +def grok_file(s): + """Separate "file,old-version,new-version".""" + m = sVv_re.match(s) + if not m: + raise ValueError, "'%s' not in file,old-vers,new-vers format" % s + return m.groups() + +failures = 0 +def note_failure(msg, *args): + """Register a failure for handle_failures to announce at the end.""" + global failures + failures = 1 + apply(complain, (msg,) + args) + +def getuser(): + """Try to get the user's login name.""" + try: return getpass.getuser() + except: return None + +def get_user_ldap_info(user): + """Obtain some aproximation to user's (fullname, email). + + We prefer to get the info out of ldap. + + Failing that, we use whatever we can get from the password file. + + Failing that, we return an empty fullname and the value of OFFICIAL_SENDER. + + Failling all, we return ('', '').""" + if not user: + return ('', '') + # Fallback values: + email = OFFICIAL_SENDER + try: + fullname = pwd.getpwnam(user)[4] + except KeyError: + fullname = '' + + if USE_LDAP: + try: + import ldap; ldap_mod = ldap + except ImportError: + try: + import _ldap; ldap_mod = _ldap + except ImportError: + print "Failed to get any LDAP module, punting on ldap." + return (fullname, email) + try: + c = ldap_mod.open(LDAP_HOST, LDAP_PORT) + c.simple_bind_s('', '') + # ('sn' for "surname") + record = c.search_s('ou=people,o=DC,c=US', + ldap_mod.SCOPE_SUBTREE, + 'loginName=%s' % user, + ['mail', 'sn', 'givenName']) + if record: + d = record[0][1] + email = d.get('mail', [email])[0] + first = d.get('givenName', [''])[0] + last = d.get('sn', [''])[0] + if first or last: + if first: first = first + " " + fullname = "%s%s" % (first, last) + except ldap_mod.LDAPError: + pass + + return (fullname, email) + +def handle_failures(argstring): + """On serious failures, send the complaints log to CVSMASTER.""" + + if os.environ.has_key('HOSTNAME'): + host = os.environ['HOSTNAME'] + else: + host = socket.gethostbyaddr(socket.gethostname())[0] + if not host: + host = "nohost.zope.com" + + user = getuser() or "unidentified" + + if os.path.isfile(OUTPUT_LOG): + log_file_expr = ("\n\tSee log file for application errors:\n\t\t%s" + % OUTPUT_LOG) + else: + log_file_expr = "" + + complain("Sending complaints log to CVSMASTER %s\n", CVSMASTER) + complain("Time stamp: %s\n", time.ctime(time.time())) + complain("Fatal failures in %s:%s" + "\n\tCheckin by: %s@%s\n\tInvocation: %s\n\n", + SCRIPT, log_file_expr, user, host, argstring) + + cmd = ('%s %s -s "CVS errors in %s for %s" %s' + % (MAIL_CMD, ((VERBOSE and "-v") or ""), SCRIPT, user, CVSMASTER)) + f = os.popen(cmd, 'w') + f.write("Serious errors encountered during CVS postcommit actions,\n") + f.write("the log is below with orienting details at the bottom.\n\n") + f.write(string.join(complaints, "")) + return f.close() + +def usage(): + complain("Usage: %s [options] ", SCRIPT) + complain('"repopath file,oldv,newv [f2,o2,n2 ...]"\n') + complain("(Note that repo-path and files must be" + " quoted so they are a single token.)\n") + +def get_command(cmd, path): + """Get a valid exe for cmd on path.""" + for d in path: + maybe = os.path.join(d, cmd) + if os.path.isfile(maybe): + return maybe + note_failure("No usable %s executable found.\n", `cmd`) + +EXPANDS_RE = re.compile("expand.*@b@") +def expands_as_binary(repository_path): + """Return true if the repository file is marked for binary keyword + expansion.""" + + f = open(repository_path, 'r') + while 1: + l = f.readline().strip() + if not l: + # Break out on first blank line or eof: + return 0 + if EXPANDS_RE.match(l): + return 1 + +complaints = [] +def complain(msg, *args): + global complaints + + import time + t = time.strftime("%Y-%m-%d %T - ", time.localtime(time.time())) + + complaints.append(t + (msg % args)) + sys.stderr.write(msg % args) + sys.stderr.flush() + +def safe_getcwd(fallback): + try: + return os.getcwd() + except: + os.chdir(fallback) + try: + return os.getcwd() + except: + return "" + +if __name__ == "__main__": + main(sys.argv) Index: CVSROOT/repolinks =================================================================== RCS file: /usr/local/cvsroot/CVSROOT/repolinks,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ CVSROOT/repolinks 9 Feb 2004 13:13:44 -0000 1.1 @@ -0,0 +1,29 @@ +# Recipe file for symlinks in the repository. + +# When checked in, this file is read and the specified link structure +# is imposed on the repository. Any existing repository links not +# specified in this file are deleted. Any links specified here that +# wind up pointing outside the repository are also deleted. + +# Blank lines and lines beginning with '#' are ignored. + +# The other lines should contain one or two white-space separated paths: + +# - The first path indicates where the symlink is situated. It is +# interepreted relative to the repository root, whether or not it +# starts with a '/'. + +# - The second path indicates the target of the link. It is +# interpreted relative to the repo root if it is absolute, otherwise +# it is used as is, for relative links within the repository. +# +# The second path can be empty, to indicate retirement of an +# existing symlink. You should not just remove entries for symlinks +# - their absence will break checkouts that included them, unless +# they're replaced by actual directories. Not specifying the second +# path translates to a link that points to an empty directory +# (CVSROOT/Emptydir) - the CVS equivalent of a removed directory. + +#################### ##################### +# Where Link Lives # # Where Link Points # +#################### ##################### Index: CVSROOT/traffic_table.py =================================================================== RCS file: /usr/local/cvsroot/CVSROOT/traffic_table.py,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ CVSROOT/traffic_table.py 9 Feb 2004 13:13:44 -0000 1.1 @@ -0,0 +1,75 @@ +# Adapted by bdolicki for openacs.org +# Changes to http://dev.zope.org/CVS/traffic_table.py +# 001 Email addresses +# 002 Added /openacs-4 pattern +# 003 No catchall + +"""Table dictating what goes where for the traffic_cop module. + +The global var 'table' contains a list of entries identifying where +change-notifications for particular sections of the repository are sent. + +Each 'table' entry is a dictionary containing some mandatory and some +optional fields (optional fields have default values described) + + - 'addrs' - a list of email addresses for checkin notice delivery. + + - 'path' - to the repository dir, relative to the CVSROOT. If the + leading path of the files being checked in (re) match the value of this + attribute, then this entry is invoked. + + NOTE that the comparison mechanism takes into account the repository + symlinks (as dictated by the repolinks file). This means that all entries + for directories that contain checked-in files by virtue of symlinks, as + well as by direct containment, will qualify - the system takes care of + unravelling the symlinks for you. +""" + +# 001 +#internal = ["project-cvs@my.YOURHOST.com", "project-log@my.YOURHOST.com"] +# Some openacs.org aliases would be helpful here +internal = ["openacs-cvs-list-admin@willfork.com", "davis@xarg.net"] + + +_TABLE = [] + +def add_to_table(entries, prepend=0): + global _TABLE + if type(entries) not in [type([]), type(())]: + entries = [entries] + if prepend: + _TABLE = entries + _TABLE + else: + _TABLE = _TABLE + entries + +def add_multipath(paths, addrs): + """Add entries with different paths but the same addrs and remote""" + for path in paths: + add_to_table({'path': path, + 'addrs': addrs}) + +def get_table(): + return _TABLE[:] + +def init_table(): + add_to_table([ + # CVSROOT entry is crucial: + {'path': "CVSROOT", + 'addrs': internal, + 'specials': [("repolinks", "adjustlinks.py")], + 'verbose': 1}, + +# 002 +# {'path': "PrjChat", +# 'addrs': ["project-chat-cvs@my.zope.com", +# "project-chat-log@my.zope.com"]}, + {'path': "openacs-4", + 'addrs': ["openacs-cvs-list@willfork.com"]}, + +# 003 +# # Catchall for when *no* other entry matches: +# {'path': None, +# 'addrs': internal}, + ]) + +init_table()