You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by gs...@apache.org on 2012/02/28 19:52:13 UTC

svn commit: r1294778 - in /subversion/trunk/tools/server-side/svnpubsub: ./ rc.d/ svnpubsub/

Author: gstein
Date: Tue Feb 28 18:52:13 2012
New Revision: 1294778

URL: http://svn.apache.org/viewvc?rev=1294778&view=rev
Log:
Add the SvnPubSub framework.

This is copied from r806478 of:
  https://svn.apache.org/repos/infra/infrastructure/trunk/projects/svnpubsub

History has not been carried over. See the original source for history.

Added:
    subversion/trunk/tools/server-side/svnpubsub/
    subversion/trunk/tools/server-side/svnpubsub/README.txt
    subversion/trunk/tools/server-side/svnpubsub/commit-hook.py   (with props)
    subversion/trunk/tools/server-side/svnpubsub/example.conf
    subversion/trunk/tools/server-side/svnpubsub/rc.d/
    subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub   (with props)
    subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian   (with props)
    subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris   (with props)
    subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub   (with props)
    subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian   (with props)
    subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris   (with props)
    subversion/trunk/tools/server-side/svnpubsub/svnpubsub/
    subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac
    subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py
    subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py
    subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py
    subversion/trunk/tools/server-side/svnpubsub/svntweet.py
    subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py   (with props)
    subversion/trunk/tools/server-side/svnpubsub/test.conf
    subversion/trunk/tools/server-side/svnpubsub/testserver.py   (with props)
    subversion/trunk/tools/server-side/svnpubsub/watcher.py   (with props)

Added: subversion/trunk/tools/server-side/svnpubsub/README.txt
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/README.txt?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/README.txt (added)
+++ subversion/trunk/tools/server-side/svnpubsub/README.txt Tue Feb 28 18:52:13 2012
@@ -0,0 +1 @@
+### write a README

Added: subversion/trunk/tools/server-side/svnpubsub/commit-hook.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/commit-hook.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/commit-hook.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/commit-hook.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,89 @@
+#!/usr/local/bin/python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+SVNLOOK="/usr/local/svn-install/current/bin/svnlook"
+#SVNLOOK="/usr/local/bin/svnlook"
+
+import sys
+import subprocess
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+import urllib2
+
+HOST="127.0.0.1"
+PORT=2069
+
+def svncmd(cmd):
+    return subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+
+def svncmd_uuid(repo):
+    cmd = "%s uuid %s" % (SVNLOOK, repo)
+    p = svncmd(cmd)
+    return p.stdout.read().strip()
+
+def svncmd_info(repo, revision):
+    cmd = "%s info -r %s %s" % (SVNLOOK, revision, repo)
+    p = svncmd(cmd)
+    data = p.stdout.read().strip().split("\n")
+    #print data
+    return {'author': data[0],
+            'date': data[1],
+            'log': "".join(data[3:])}
+
+def svncmd_dirs(repo, revision):
+    cmd = "%s dirs-changed  -r %s %s" % (SVNLOOK, revision, repo)
+    p = svncmd(cmd)
+    dirs = []
+    while True:
+        line = p.stdout.readline()
+        if not line:
+            break
+        dirs.append(line.strip())
+    return dirs
+
+def do_put(body):
+    opener = urllib2.build_opener(urllib2.HTTPHandler)
+    request = urllib2.Request("http://%s:%d/dirs-changed" %(HOST, PORT), data=body)
+    request.add_header('Content-Type', 'application/json')
+    request.get_method = lambda: 'PUT'
+    url = opener.open(request)
+
+
+def main(repo, revision):
+    i = svncmd_info(repo, revision)
+    data = {'revision': int(revision),
+            'dirs_changed': [],
+            'repos': svncmd_uuid(repo),
+            'author': i['author'],
+            'log': i['log'],
+            'date': i['date'],
+            }
+    data['dirs_changed'].extend(svncmd_dirs(repo, revision))
+    body = json.dumps(data)
+    #print body
+    do_put(body)
+
+if __name__ == "__main__":
+    if len(sys.argv) != 3:
+        print "invalid args"
+        sys.exit(0)
+
+    main(sys.argv[1], sys.argv[2])

Propchange: subversion/trunk/tools/server-side/svnpubsub/commit-hook.py
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/example.conf
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/example.conf?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/example.conf (added)
+++ subversion/trunk/tools/server-side/svnpubsub/example.conf Tue Feb 28 18:52:13 2012
@@ -0,0 +1,116 @@
+### turn this into an example
+
+[DEFAULT]
+svnbin: /usr/local/bin/svn
+streams: http://svn-master.apache.org:2069/commits/xml
+         http://dist.apache.org:2069/commits/xml
+
+ASF: http://svn-master.apache.org/repos/asf
+INFRA: https://svn-master.apache.org/repos/infra
+CMS: %(INFRA)s/websites/production
+DIST: https://dist.apache.org/repos/dist
+
+[env]
+HOME: /home/svnwc
+LANG: en_US.UTF-8
+
+[track]
+/x1/www/apr.apache.org: %(ASF)s/apr/site/trunk/docs
+/x1/www/apr.apache.org/dev/dist: %(DIST)s/dev/apr
+/x1/www/www.apache.org/dist/apr: %(DIST)s/release/apr
+/x1/www/www.apache.org/dist/poi: %(DIST)s/release/poi
+/x1/www/www.apache.org/dist/pivot: %(DIST)s/release/pivot
+/x1/www/www.apache.org/dist/httpd: %(DIST)s/release/httpd
+/x1/www/www.apache.org/dist/incubator/airavata: %(DIST)s/release/incubator/airavata
+/x1/www/www.apache.org/: %(CMS)s/www
+/x1/www/httpd.apache.org/: %(ASF)s/httpd/site/trunk/docs
+/x1/www/httpd.apache.org/docs/2.0: %(ASF)s/httpd/httpd/branches/2.0.x/docs/manual
+/x1/www/httpd.apache.org/docs/2.2: %(ASF)s/httpd/httpd/branches/2.2.x/docs/manual
+/x1/www/httpd.apache.org/docs/1.3: %(ASF)s/httpd/httpd/branches/1.3.x/htdocs/manual
+/x1/www/httpd.apache.org/docs/2.4: %(ASF)s/httpd/httpd/branches/2.4.x/docs/manual
+/x1/www/httpd.apache.org/docs/trunk: %(ASF)s/httpd/httpd/trunk/docs/manual
+/x1/www/httpd.apache.org/mod_fcgid/mod: %(ASF)s/httpd/mod_fcgid/trunk/docs/manual/mod
+/x1/www/httpd.apache.org/mod_fcgid/style: %(ASF)s/httpd/httpd/trunk/docs/manual/style
+/x1/www/httpd.apache.org/mod_fcgid/images: %(ASF)s/httpd/httpd/trunk/docs/manual/images
+/x1/www/httpd.apache.org/dev/dist: %(DIST)s/dev/httpd
+/x1/www/httpd.apache.org/mod_ftp/mod: %(ASF)s/httpd/mod_ftp/trunk/docs/manual/mod
+/x1/www/httpd.apache.org/mod_ftp/style: %(ASF)s/httpd/httpd/trunk/docs/manual/style
+/x1/www/httpd.apache.org/mod_ftp/images: %(ASF)s/httpd/httpd/trunk/docs/manual/images
+/x1/www/httpd.apache.org/mod_ftp/ftp: %(ASF)s/httpd/mod_ftp/trunk/docs/manual/ftp
+/x1/www/libcloud.apache.org: %(CMS)s/libcloud
+/x1/www/river.apache.org: %(CMS)s/river/content
+/x1/www/incubator.apache.org/stanbol: %(CMS)s/stanbol/content/stanbol
+/x1/www/incubator.apache.org/kitty: %(CMS)s/kitty/content/kitty
+/x1/www/www.apache.org/dist/trafficserver: %(DIST)s/release/trafficserver
+/x1/staging/harmony.apache.org/eclipse/update: %(ASF)s/harmony/enhanced/tools/trunk/eclipse/org.apache.harmony.eclipse.site
+/x1/staging/harmony.apache.org/externals/security: %(ASF)s/harmony/enhanced/java/trunk/classlib/doc/security
+/x1/staging/harmony.apache.org/externals/regex: %(ASF)s/harmony/enhanced/java/trunk/classlib/doc/regex
+/x1/staging/harmony.apache.org: %(ASF)s/harmony/standard/site/trunk/docs
+/x1/www/harmony.apache.org/eclipse/update: %(ASF)s/harmony/enhanced/tools/trunk/eclipse/org.apache.harmony.eclipse.site
+/x1/www/harmony.apache.org/externals/security: %(ASF)s/harmony/enhanced/java/trunk/classlib/doc/security
+/x1/www/harmony.apache.org/externals/regex: %(ASF)s/harmony/enhanced/java/trunk/classlib/doc/regex
+/x1/www/harmony.apache.org: %(ASF)s/harmony/standard/site/branches/live/docs
+/x1/www/subversion.apache.org/: %(ASF)s/subversion/site
+/x1/www/trafficserver.apache.org/: %(CMS)s/trafficserver
+/x1/www/qpid.apache.org: %(ASF)s/qpid/site/docs
+/x1/www/pdfbox.apache.org: %(ASF)s/pdfbox/site/publish
+/x1/www/cassandra.apache.org: %(ASF)s/cassandra/site/publish
+/x1/www/community.apache.org: %(CMS)s/community
+/x1/www/nutch.apache.org: %(ASF)s/nutch/site/publish
+/x1/www/wicket.apache.org: %(ASF)s/wicket/common/site/trunk/_site
+/x1/www/wicket.apache.org/apidocs: %(ASF)s/wicket/common/site/apidocs
+/x1/www/incubator.apache.org/callback: %(CMS)s/callback/content/callback
+/x1/www/incubator.apache.org/zetacomponents: %(ASF)s/incubator/zetacomponents/website/htdocs
+/x1/www/incubator.apache.org/flex: %(CMS)s/flex/content/flex
+/x1/www/incubator.apache.org/jena: %(CMS)s/jena/content/jena
+/x1/www/incubator.apache.org/celix: %(CMS)s/celix/content/celix
+/x1/www/incubator.apache.org/lucene.net: %(CMS)s/lucene.net/content/lucene.net
+/x1/www/incubator.apache.org/easyant: %(ASF)s/incubator/easyant/site/production
+/x1/www/incubator.apache.org/etch: %(CMS)s/etch/content/etch
+/x1/www/incubator.apache.org/rave: %(CMS)s/rave/content/rave
+/x1/www/incubator.apache.org/wave: %(CMS)s/wave/content/wave
+/x1/www/incubator.apache.org/lucy: %(CMS)s/lucy/content/lucy
+/x1/www/incubator.apache.org/openmeetings: %(ASF)s/incubator/openmeetings/trunk/singlewebapp/docs
+/x1/www/incubator.apache.org/openofficeorg: %(CMS)s/openofficeorg/content/openofficeorg
+/x1/www/incubator.apache.org/odftoolkit: %(CMS)s/odftoolkit/content/odftoolkit
+/x1/www/incubator.apache.org/airavata: %(CMS)s/airavata/content/airavata
+/x1/www/incubator.apache.org/wookie: %(CMS)s/wookie/content/wookie
+/x1/www/incubator.apache.org/accumulo: %(CMS)s/accumulo/content/accumulo
+/x1/www/gora.apache.org: %(ASF)s/gora/site/publish
+/x1/www/incubator.apache.org/devicemap: %(CMS)s/devicemap/content/devicemap
+/x1/www/aries.apache.org: %(CMS)s/aries/content
+/x1/www/tika.apache.org: %(ASF)s/tika/site/publish
+/x1/www/uima.apache.org/pubsub: %(ASF)s/uima/site/trunk/uima-website/docs
+/x1/www/zookeeper.apache.org: %(CMS)s/zookeeper
+/x1/www/chemistry.apache.org: %(CMS)s/chemistry
+/x1/www/ant.apache.org: %(ASF)s/ant/site/ant/production
+/x1/www/ant.apache.org/ivy: %(ASF)s/ant/site/ivy/production
+/x1/www/ant.apache.org/ivy/ivyde: %(ASF)s/ant/site/ivyde/production
+/x1/www/www.apache.org/dist/esme: %(DIST)s/release/esme
+/x1/www/www.apache.org/dist/libcloud: %(DIST)s/release/libcloud
+/x1/www/archive.apachecon.com: %(INFRA)s/apachecon/archive.apachecon.com
+/x1/www/oodt.apache.org: %(ASF)s/oodt/site
+/x1/www/esme.apache.org: %(CMS)s/esme/content
+/x1/www/ooo-site.apache.org: %(CMS)s/ooo-site
+/x1/www/openejb.apache.org: %(CMS)s/openejb
+/x1/www/deltacloud.apache.org: %(ASF)s/deltacloud/trunk/site/output
+/x1/www/ace.apache.org: %(CMS)s/ace
+/x1/www/stdcxx.apache.org/doc: %(ASF)s/stdcxx/trunk/doc
+/x1/www/stdcxx.apache.org: %(ASF)s/stdcxx/site
+/x1/www/www.apache.org/dist/tomcat: %(DIST)s/release/tomcat
+/x1/www/incubator.apache.org/any23: %(ASF)s/incubator/any23/site
+/x1/www/incubator.apache.org/bloodhound: %(ASF)s/incubator/bloodhound/site
+/x1/www/labs.apache.org/: %(CMS)s/labs
+/x1/www/lucene.apache.org: %(CMS)s/lucene
+/x1/www/lucene.apache.org/content/core/old_versioned_docs: %(ASF)s/lucene/old_versioned_docs
+/x1/www/pivot.apache.org: %(ASF)s/pivot/site/trunk/deploy
+/x1/www/www.apache.org/dist/empire-db: %(DIST)s/release/empire-db
+/x1/www/empire-db.apache.org: %(ASF)s/empire-db/site
+/x1/www/www.apache.org/dist/subversion: %(DIST)s/release/subversion
+/x1/www/avro.apache.org: %(ASF)s/avro/site/publish
+/x1/www/incubator.apache.org/cordova: %(ASF)s/incubator/cordova/site/public
+/x1/www/mahout.apache.org: %(ASF)s/mahout/site/new_website
+/x1/www/opennlp.apache.org: %(CMS)s/opennlp
+/x1/www/gump.apache.org: %(ASF)s/gump/site
+/x1/www/incubator.apache.org/syncope: %(ASF)s/incubator/syncope/site
+/x1/staging/www.apache.org: %(ASF)s/infrastructure/site/branches/flamebait/docs

Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub Tue Feb 28 18:52:13 2012
@@ -0,0 +1,35 @@
+#!/bin/sh
+#
+# PROVIDE: svnpubsub
+# REQUIRE: DAEMON
+# KEYWORD: shutdown
+
+. /etc/rc.subr
+
+name="svnpubsub"
+rcvar=`set_rcvar`
+
+load_rc_config $name
+
+#
+# DO NOT CHANGE THESE DEFAULT VALUES HERE
+# SET THEM IN THE /etc/rc.conf FILE
+#
+svnpubsub_enable=${svnpubsub_enable-"NO"}
+svnpubsub_user=${svnpubsub_user-"svn"}
+svnpubsub_group=${svnpubsub_group-"svn"}
+svnpubsub_reactor=${svnpubsub_reactor-"poll"}
+svnpubsub_pidfile=${svnpubsub_pidfile-"/var/run/svnpubsub/svnpubsub.pid"}
+pidfile="${svnpubsub_pidfile}"
+
+export PYTHON_EGG_CACHE="/home/svn/.python-eggs"
+
+command="/usr/local/bin/twistd"
+command_args="-y /usr/local/svnpubsub/svnpubsub.tac \
+            --logfile=/var/log/vc/svnpubsub.log \
+            --pidfile=${pidfile} \
+            --uid=${svnpubsub_user} --gid=${svnpubsub_user} \
+            -r${svnpubsub_reactor}"
+
+
+run_rc_command "$1"

Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian Tue Feb 28 18:52:13 2012
@@ -0,0 +1,62 @@
+#!/bin/bash
+### BEGIN INIT INFO
+# Provides:          svnpubsub
+# Required-Start:    $remote_fs
+# Required-Stop:     $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: SvnPubSub
+# Description:       start SvnPubSub daemon
+#### END INIT INFO
+
+. /lib/init/vars.sh
+. /lib/lsb/init-functions
+
+svnpubsub_user=${svnpubsub_user-"daemon"}
+svnpubsub_group=${svnpubsub_group-"daemon"}
+svnpubsub_reactor=${svnpubsub_reactor-"poll"}
+svnpubsub_pidfile=${svnpubsub_pidfile-"/var/run/svnpubsub.pid"}
+pidfile="${svnpubsub_pidfile}"
+
+TWSITD_CMD="/usr/bin/twistd -y /opt/svnpubsub/svnpubsub.tac \
+            --logfile=/var/bwlog/svnpubsub/svnpubsub.log \
+            --pidfile=${pidfile} \
+            --uid=${svnpubsub_user} --gid=${svnpubsub_user} \
+            -r${svnpubsub_reactor}"
+
+RETVAL=0
+ 
+start() {
+    echo "Starting SvnPubSub Server: "
+    $TWSITD_CMD
+    RETVAL=$?
+    [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+    return $RETVAL
+}
+ 
+stop() {
+    echo "Stopping SvnPubSub Server: "
+    THE_PID=`cat ${pidfile}`
+    kill $THE_PID
+    RETVAL=$?
+    [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+    return $RETVAL
+}
+
+case "$1" in
+    start)
+        start
+        ;;
+    stop)
+        stop
+        ;;
+    restart)
+        stop
+        start
+        ;;
+    *)
+        echo "Usage: $0 {start|stop|restart}"
+        exit 1
+esac
+
+exit $RETVAL

Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris Tue Feb 28 18:52:13 2012
@@ -0,0 +1,53 @@
+#!/usr/bin/bash
+#
+# a dumb init script for twistd on solaris. cus like, writing XML for SMF is f'ing lame.
+#
+
+svnpubsub_user=${svnpubsub_user-"daemon"}
+svnpubsub_group=${svnpubsub_group-"daemon"}
+svnpubsub_reactor=${svnpubsub_reactor-"poll"}
+svnpubsub_pidfile=${svnpubsub_pidfile-"/var/run/svnpubsub/svnpubsub.pid"}
+pidfile="${svnpubsub_pidfile}"
+
+TWSITD_CMD="/opt/local/bin//twistd -y /usr/local/svnpubsub/svnpubsub.tac \
+            --logfile=/x1/log/svnpubsub.log \
+            --pidfile=${pidfile} \
+            --uid=${svnpubsub_user} --gid=${svnpubsub_user} \
+            -r${svnpubsub_reactor}"
+
+RETVAL=0
+ 
+start() {
+    echo "Starting SvnPubSub Server: "
+    $TWSITD_CMD
+    RETVAL=$?
+    [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+    return $RETVAL
+}
+ 
+stop() {
+    echo "Stopping SvnPubSub Server: "
+    THE_PID=`cat ${pidfile}`
+    kill $THE_PID
+    RETVAL=$?
+    [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+    return $RETVAL
+}
+
+case "$1" in
+    start)
+        start
+        ;;
+    stop)
+        stop
+        ;;
+    restart)
+        stop
+        start
+        ;;
+    *)
+        echo "Usage: $0 {start|stop|restart}"
+        exit 1
+esac
+
+exit $RETVAL

Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub Tue Feb 28 18:52:13 2012
@@ -0,0 +1,39 @@
+#!/bin/sh
+#
+# PROVIDE: svnwcsub
+# REQUIRE: DAEMON
+# KEYWORD: shutdown
+
+. /etc/rc.subr
+
+name="svnwcsub"
+rcvar=`set_rcvar`
+
+load_rc_config $name
+
+#
+# DO NOT CHANGE THESE DEFAULT VALUES HERE
+# SET THEM IN THE /etc/rc.conf FILE
+#
+svnwcsub_enable=${svnwcsub_enable-"NO"}
+svnwcsub_user=${svnwcsub_user-"svnwc"}
+svnwcsub_group=${svnwcsub_group-"svnwc"}
+svnwcsub_reactor=${svnwcsub_reactor-"poll"}
+svnwcsub_pidfile=${svnwcsub_pidfile-"/var/run/svnwcsub/svnwcsub.pub"}
+svnwcsub_program=${svnwcsub_program-"/usr/local/bin/twistd"}
+svnwcsub_env="PYTHON_EGG_CACHE"
+svnwcsub_cmd_int=${svnwcsub_cmd_int-"python"}
+pidfile="${svnwcsub_pidfile}"
+
+export PYTHON_EGG_CACHE="/var/run/svnwcsub"
+
+command="/usr/local/bin/twistd"
+command_interpreter="/usr/local/bin/${svnwcsub_cmd_int}"
+command_args="-y /usr/local/svnpubsub/svnwcsub.tac \
+            --logfile=/var/log/svnwcsub.log \
+            --pidfile=${pidfile} \
+            --uid=${svnwcsub_user} --gid=${svnwcsub_group} \
+            --umask=002 -r${svnwcsub_reactor}"
+
+run_rc_command "$1"
+

Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian Tue Feb 28 18:52:13 2012
@@ -0,0 +1,62 @@
+#!/bin/bash
+### BEGIN INIT INFO
+# Provides:          svnwcsub
+# Required-Start:    $remote_fs
+# Required-Stop:     $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: SvnWcSub
+# Description:       start SvnWcSub daemon
+#### END INIT INFO
+
+. /lib/init/vars.sh
+. /lib/lsb/init-functions
+
+svnwcsub_user=${svnwcsub_user-"svnwc"}
+svnwcsub_group=${svnwcsub_group-"svnwc"}
+svnwcsub_reactor=${svnwcsub_reactor-"poll"}
+svnwcsub_pidfile=${svnwcsub_pidfile-"/var/run/svnwcsub.pid"}
+pidfile="${svnwcsub_pidfile}"
+
+TWSITD_CMD="/usr/bin/twistd -y /opt/svnpubsub/svnwcsub.tac \
+            --logfile=/var/bwlog/svnpubsub/svnwcsub.log \
+            --pidfile=${pidfile} \
+            --uid=${svnwcsub_user} --gid=${svnwcsub_group} \
+            -r${svnwcsub_reactor}"
+
+RETVAL=0
+ 
+start() {
+    echo "Starting SvnWcSub Server: "
+    $TWSITD_CMD
+    RETVAL=$?
+    [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+    return $RETVAL
+}
+ 
+stop() {
+    echo "Stopping SvnWcSub Server: "
+    THE_PID=`cat ${pidfile}`
+    kill $THE_PID
+    RETVAL=$?
+    [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+    return $RETVAL
+}
+
+case "$1" in
+    start)
+        start
+        ;;
+    stop)
+        stop
+        ;;
+    restart)
+        stop
+        start
+        ;;
+    *)
+        echo "Usage: $0 {start|stop|restart}"
+        exit 1
+esac
+
+exit $RETVAL

Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris Tue Feb 28 18:52:13 2012
@@ -0,0 +1,54 @@
+#!/usr/bin/bash
+#
+# a dumb init script for twistd on solaris. cus like, writing XML for SMF is f'ing lame.
+#
+
+svnwcsub_user=${svnwcsub_user-"svnwc"}
+svnwcsub_group=${svnwcsub_group-"other"}
+svnwcsub_reactor=${svnwcsub_reactor-"poll"}
+svnwcsub_pidfile=${svnwcsub_pidfile-"/var/run/svnwcsub/svnwcsub.pid"}
+pidfile="${svnwcsub_pidfile}"
+
+TWSITD_CMD="/opt/python/2.6.2/bin/twistd -y /usr/local/svnpubsub/svnwcsub.tac \
+            --logfile=/x1/log/svnwcsub.log \
+            --pidfile=${pidfile} \
+            --umask=002 \
+            --uid=${svnwcsub_user} --gid=${svnwcsub_group} \
+            -r${svnwcsub_reactor}"
+
+RETVAL=0
+ 
+start() {
+    echo "Starting SvnWcSub Server: "
+    $TWSITD_CMD
+    RETVAL=$?
+    [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+    return $RETVAL
+}
+ 
+stop() {
+    echo "Stopping SvnWcSub Server: "
+    THE_PID=`cat ${pidfile}`
+    kill $THE_PID
+    RETVAL=$?
+    [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+    return $RETVAL
+}
+
+case "$1" in
+    start)
+        start
+        ;;
+    stop)
+        stop
+        ;;
+    restart)
+        stop
+        start
+        ;;
+    *)
+        echo "Usage: $0 {start|stop|restart}"
+        exit 1
+esac
+
+exit $RETVAL

Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac Tue Feb 28 18:52:13 2012
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import sys
+import os
+from twisted.application import service, internet
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from svnpubsub.server import svnpubsub_server
+
+application = service.Application("SvnPubSub")
+
+def get_service():
+    return internet.TCPServer(2069, svnpubsub_server())
+
+service = get_service()
+service.setServiceParent(application)

Added: subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1 @@
+# Turn svnpubsub/ into a package.

Added: subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,232 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Generic client for SvnPubSub
+#
+# ### usage...
+#
+#
+# EVENTS
+#
+#   connected: a connection to the server has been opened (though not
+#                 necessarily established)
+#   closed:    the connection was closed. reconnect will be attempted.
+#   error:     an error closed the connection. reconnect will be attempted.
+#   ping:      the server has sent a keepalive
+#   stale:     no activity has been seen, so the connection will be closed
+#                 and reopened
+#
+
+import asyncore
+import asynchat
+import socket
+import functools
+import time
+import xml.sax
+
+# How long the polling loop should wait for activity before returning.
+TIMEOUT = 30.0
+
+# Always delay a bit when trying to reconnect. This is not precise, but sets
+# a minimum amount of delay. At the moment, there is no further backoff.
+RECONNECT_DELAY = 25.0
+
+# If we don't see anything from the server for this amount time, then we
+# will drop and reconnect. The TCP connection may have gone down without
+# us noticing it somehow.
+STALE_DELAY = 60.0
+
+
+class Client(asynchat.async_chat):
+
+  def __init__(self, host, port, commit_callback, event_callback):
+    asynchat.async_chat.__init__(self)
+
+    self.last_activity = time.time()
+
+    self.host = host
+    self.port = port
+    self.event_callback = event_callback
+
+    handler = XMLStreamHandler(commit_callback, event_callback)
+
+    self.parser = xml.sax.make_parser(['xml.sax.expatreader'])
+    self.parser.setContentHandler(handler)
+
+    # Wait for the end of headers. Then we start parsing XML.
+    self.set_terminator('\r\n\r\n')
+    self.skipping_headers = True
+
+    self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+    self.connect((host, port))
+    ### should we allow for repository restrictions?
+    self.push('GET /commits/xml HTTP/1.0\r\n\r\n')
+
+  def handle_connect(self):
+    self.event_callback('connected')
+
+  def handle_close(self):
+    self.event_callback('closed')
+    self.close()
+
+  def handle_error(self):
+    self.event_callback('error')
+    self.close()
+
+  def found_terminator(self):
+    self.skipping_headers = False
+
+    # From here on, collect everything. Never look for a terminator.
+    self.set_terminator(None)
+
+  def collect_incoming_data(self, data):
+    # Remember the last time we saw activity
+    self.last_activity = time.time()
+
+    if not self.skipping_headers:
+      # Just shove this into the XML parser. As the elements are processed,
+      # we'll collect them into an appropriate structure, and then invoke
+      # the callback when we have fully received a commit.
+      self.parser.feed(data)
+
+
+class XMLStreamHandler(xml.sax.handler.ContentHandler):
+
+  def __init__(self, commit_callback, event_callback):
+    self.commit_callback = commit_callback
+    self.event_callback = event_callback
+
+    self.rev = None
+    self.chars = ''
+
+  def startElement(self, name, attrs):
+    if name == 'commit':
+      self.rev = Revision(attrs['repository'], attrs['revision'])
+    # No other elements to worry about.
+
+  def characters(self, data):
+    self.chars += data
+
+  def endElement(self, name):
+    if name == 'commit':
+      self.commit_callback(self.rev)
+      self.rev = None
+    elif name == 'stillalive':
+      self.event_callback('ping')
+    elif self.chars and self.rev:
+      value = self.chars.strip()
+      if name == 'path':
+        self.rev.dirs_changed.append(value)
+      elif name == 'author':
+        self.rev.author = value
+      elif name == 'date':
+        self.rev.date = value
+      elif name == 'log':
+        self.rev.log = value
+
+    # Toss out any accumulated characters for this element.
+    self.chars = ''
+
+
+class Revision(object):
+  def __init__(self, repos, rev):
+    self.repos = repos
+    self.rev = rev
+    self.dirs_changed = [ ]
+    self.author = None
+    self.date = None
+    self.log = None
+
+
+class MultiClient(object):
+  def __init__(self, hostports, commit_callback, event_callback):
+    self.commit_callback = commit_callback
+    self.event_callback = event_callback
+
+    # No target time, as no work to do
+    self.target_time = 0
+    self.work_items = [ ]
+
+    for host, port in hostports:
+      self._add_channel(host, port)
+
+  def _reconnect(self, host, port, event_name):
+    if event_name == 'closed' or event_name == 'error':
+      # Stupid connection closed for some reason. Set up a reconnect. Note
+      # that it should have been removed from asyncore.socket_map already.
+      self._reconnect_later(host, port)
+
+    # Call the user's callback now.
+    self.event_callback(host, port, event_name)
+
+  def _reconnect_later(self, host, port):
+    # Set up a work item to reconnect in a little while.
+    self.work_items.append((host, port))
+
+    # Only set a target if one has not been set yet. Otherwise, we could
+    # create a race condition of continually moving out towards the future
+    if not self.target_time:
+      self.target_time = time.time() + RECONNECT_DELAY
+
+  def _add_channel(self, host, port):
+    # Simply instantiating the client will install it into the global map
+    # for processing in the main event loop.
+    Client(host, port,
+           functools.partial(self.commit_callback, host, port),
+           functools.partial(self._reconnect, host, port))
+
+  def _check_stale(self):
+    now = time.time()
+    for client in asyncore.socket_map.values():
+      if client.last_activity + STALE_DELAY < now:
+        # Whoops. No activity in a while. Signal this fact, Close the
+        # Client, then have it reconnected later on.
+        self.event_callback(client.host, client.port, 'stale')
+
+        # This should remove it from .socket_map.
+        client.close()
+
+        self._reconnect_later(client.host, client.port)
+
+  def _maybe_work(self):
+    # If we haven't reach the targetted time, or have no work to do,
+    # then fast-path exit
+    if time.time() < self.target_time or not self.work_items:
+      return
+
+    # We'll take care of all the work items, so no target for future work
+    self.target_time = 0
+
+    # Play a little dance just in case work gets added while we're
+    # currently working on stuff
+    work = self.work_items
+    self.work_items = [ ]
+
+    for host, port in work:
+      self._add_channel(host, port)
+
+  def run_forever(self):
+    while True:
+      if asyncore.socket_map:
+        asyncore.loop(timeout=TIMEOUT, count=1)
+      else:
+        time.sleep(TIMEOUT)
+
+      self._check_stale()
+      self._maybe_work()

Added: subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,271 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# SvnPubSub - Simple Push Notification of Subversion commits
+#
+# Based on the theory behind the Live Journal Atom Streaming Service:
+#   <http://atom.services.livejournal.com/>
+#
+# Instead of using a complicated XMPP/AMPQ/JMS/super messaging service,
+# we have simple HTTP GETs and PUTs to get data in and out.
+#
+# Currently supports both XML and JSON serialization.
+#
+# Example Sub clients:
+#   curl  -i http://127.0.0.1:2069/dirs-changed/xml
+#   curl  -i http://127.0.0.1:2069/dirs-changed/json
+#   curl  -i http://127.0.0.1:2069/commits/json
+#   curl  -i http://127.0.0.1:2069/commits/13f79535-47bb-0310-9956-ffa450edef68/json
+#   curl  -i http://127.0.0.1:2069/dirs-changed/13f79535-47bb-0310-9956-ffa450edef68/json
+#
+#   URL is built into 3 parts:
+#       /${type}/${optional_repo_uuid}/${format}
+#
+#   If the repository UUID is included in the URl, you will only recieve 
+#   messages about that repository.
+#
+# Example Pub clients:
+#   curl -T revinfo.json -i http://127.0.0.1:2069/commit
+#
+# TODO:
+#   - Add Real access controls (not just 127.0.0.1)
+#   - Document PUT format
+#   - Convert to twisted.python.log
+
+
+
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+import sys
+
+import twisted
+from twisted.internet import reactor
+from twisted.internet import defer
+from twisted.web import server, static
+from twisted.web import resource
+from twisted.python import log
+
+try:
+    from xml.etree import cElementTree as ET
+except:
+    from xml.etree import ElementTree as ET
+import time
+
+class Revision:
+    def __init__(self, r):
+        self.rev = r.get('revision')
+        self.repos = r.get('repos')
+        self.dirs_changed = [x.encode('unicode_escape') for x in r.get('dirs_changed')]
+        self.author = r.get('author').encode('unicode_escape')
+        self.log = r.get('log').encode('unicode_escape')
+        self.date = r.get('date').encode('unicode_escape')
+
+    def render_commit(self, format):
+        if format == "json":
+            return json.dumps({'commit': {'repository': self.repos,
+                                          'revision': self.rev,
+                                          'dirs_changed': self.dirs_changed,
+                                          'author': self.author,
+                                          'log': self.log,
+                                          'date': self.date}}) +","
+        elif format == "xml": 
+            c = ET.Element('commit', {'repository': self.repos, 'revision': "%d" % (self.rev)})
+            ET.SubElement(c, 'author').text = self.author
+            ET.SubElement(c, 'date').text = self.date
+            ET.SubElement(c, 'log').text = self.log
+            d = ET.SubElement(c, 'dirs_changed')
+            for p in self.dirs_changed:
+                x = ET.SubElement(d, 'path')
+                x.text = p
+            str = ET.tostring(c, 'UTF-8') + "\n"
+            return str[39:]
+        else:
+            raise Exception("Ooops, invalid format")
+
+    def render_dirs_changed(self, format):
+        if format == "json":
+            return json.dumps({'commit': {'repository': self.repos,
+                                          'revision': self.rev,
+                                          'dirs_changed': self.dirs_changed}}) +","
+        elif format == "xml": 
+            c = ET.Element('commit', {'repository': self.repos, 'revision': "%d" % (self.rev)})
+            d = ET.SubElement(c, 'dirs_changed')
+            for p in self.dirs_changed:
+                x = ET.SubElement(d, 'path')
+                x.text = p
+            str = ET.tostring(c, 'UTF-8') + "\n"
+            return str[39:]
+        else:
+            raise Exception("Ooops, invalid format")
+
+HEARTBEAT_TIME = 15
+
+class Client(object):
+    def __init__(self, pubsub, r, repos, fmt):
+        self.pubsub = pubsub
+        r.notifyFinish().addErrback(self.finished)
+        self.r = r
+        self.format = fmt
+        self.repos = repos
+        self.alive = True
+        log.msg("OPEN: %s:%d (%d clients online)"% (r.getClientIP(), r.client.port, pubsub.cc()+1))
+
+    def finished(self, reason):
+        self.alive = False
+        log.msg("CLOSE: %s:%d (%d clients online)"% (self.r.getClientIP(), self.r.client.port, self.pubsub.cc()))
+        try: 
+            self.pubsub.remove(self)
+        except ValueError:
+            pass
+
+    def interested_in(self, uuid):
+        if self.repos is None:
+            return True
+        if uuid == self.repos:
+            return True
+        return False
+
+    def notify(self, data):
+        self.write(data)
+
+    def start(self):
+        self.write_start()
+        reactor.callLater(HEARTBEAT_TIME, self.heartbeat, None)
+
+    def heartbeat(self, args):
+        if self.alive:
+            self.write_heartbeat()
+            reactor.callLater(HEARTBEAT_TIME, self.heartbeat, None)
+
+    def write_data(self, data):
+        self.write(data[self.format] + "\n")
+
+    """ "Data must not be unicode" is what the interfaces.ITransport says... grr. """
+    def write(self, input):
+        self.r.write(str(input))
+
+class JSONClient(Client):
+    def write_start(self):
+        self.r.setHeader('content-type', 'application/json')
+        self.write('{"commits": [\n')
+
+    def write_heartbeat(self):
+        self.write(json.dumps({"stillalive": time.time()}) + ",\n")
+
+class XMLClient(Client):
+    def write_start(self):
+        self.r.setHeader('content-type', 'application/xml')
+        self.write("<?xml version='1.0' encoding='UTF-8'?>\n<commits>")
+
+    def write_heartbeat(self):
+        self.write("<stillalive>%f</stillalive>\n" % (time.time()))
+
+class SvnPubSub(resource.Resource):
+    isLeaf = True
+    clients = {'commits': [],
+               'dirs-changed': []}
+
+    def cc(self):
+        return reduce(lambda x,y: len(x)+len(y), self.clients.values())
+
+    def remove(self, c):
+        for k in self.clients.keys():
+            self.clients[k].remove(c)
+
+    def render_GET(self, request):
+        log.msg("REQUEST: %s"  % (request.uri))
+        uri = request.uri.split('/')
+
+        request.setHeader('content-type', 'text/plain')
+        if len(uri) != 3 and len(uri) != 4:
+            request.setResponseCode(400)
+            return "Invalid path\n"
+
+        uuid = None
+        fmt = None
+        type = uri[1]
+
+        if len(uri) == 3:
+            fmt = uri[2]
+        else:
+            fmt = uri[3]
+            uuid = uri[2]
+        
+        if type not in self.clients.keys():
+            request.setResponseCode(400)
+            return "Invalid Reuqest Type\n"
+        
+        clients = {'json': JSONClient, 'xml': XMLClient}
+        clientCls = clients.get(fmt)
+        if clientCls == None:
+            request.setResponseCode(400)
+            return "Invalid Format Requested\n"
+
+        c = clientCls(self, request, uuid, fmt)
+        self.clients[type].append(c)
+        c.start()
+        return twisted.web.server.NOT_DONE_YET
+
+    def notifyAll(self, rev):
+        data = {'commits': {},
+               'dirs-changed': {}}
+        for x in ['xml', 'json']:
+            data['commits'][x] = rev.render_commit(x)
+            data['dirs-changed'][x] = rev.render_dirs_changed(x)
+
+        log.msg("COMMIT: r%d in %d paths (%d clients)" % (rev.rev,
+                                                        len(rev.dirs_changed),
+                                                        self.cc()))
+        for k in self.clients.keys():
+            for c in self.clients[k]:
+                if c.interested_in(rev.repos):
+                    c.write_data(data[k])
+
+    def render_PUT(self, request):
+        request.setHeader('content-type', 'text/plain')
+        ip = request.getClientIP()
+        if ip != "127.0.0.1":
+            request.setResponseCode(401)
+            return "Access Denied"
+        input = request.content.read()
+        #import pdb;pdb.set_trace()
+        #print "input: %s" % (input)
+        r = json.loads(input)
+        rev = Revision(r)
+        self.notifyAll(rev)
+        return "Ok"
+
+def svnpubsub_server():
+    root = static.File("/dev/null")
+    s = SvnPubSub()
+    root.putChild("dirs-changed", s)
+    root.putChild("commits", s)
+    root.putChild("commit", s)
+    return server.Site(root)
+  
+if __name__ == "__main__":
+    log.startLogging(sys.stdout)
+    # Port 2069 "HTTP Event Port", whatever, sounds good to me
+    reactor.listenTCP(2069, svnpubsub_server())
+    reactor.run()
+

Added: subversion/trunk/tools/server-side/svnpubsub/svntweet.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svntweet.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svntweet.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svntweet.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,258 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# SvnTweet - Subscribe to a SvnPubSub stream, and Twitter about it!
+#
+# Example:
+#  svntweet.py  my-config.json
+#  
+# With my-config.json containing stream paths and the twitter auth info:
+#    {"stream": "http://svn-master.apache.org:2069/commits/xml",
+#     "username": "asfcommits",
+#     "password": "MyLuggageComboIs1234"}
+#
+#
+#
+
+import threading
+import sys
+import os
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+from twisted.internet import defer, reactor, task, threads
+from twisted.python import failure, log
+from twisted.web.client import HTTPClientFactory, HTTPPageDownloader
+
+from urlparse import urlparse
+from xml.sax import handler, make_parser
+import time
+
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "twitty-twister", "lib"))
+try:
+    import twitter
+except:
+    print "Get a copy of twitty-twister from <http://github.com/dustin/twitty-twister>"
+    sys.exit(-1)
+class Config(object):
+    def __init__(self, path):
+        self.path = path
+        self.mtime_path = 0
+        self.config = {}
+        self._load_config()
+
+    def _load_config(self):
+        mtime = os.path.getmtime(self.path)
+        if mtime != self.mtime_path:
+            fp = open(self.path, "rb")
+            self.mtime_path = mtime
+            self.config = json.loads(fp.read())
+
+class HTTPStream(HTTPClientFactory):
+    protocol = HTTPPageDownloader
+
+    def __init__(self, url):
+        HTTPClientFactory.__init__(self, url, method="GET", agent="SvnTweet/0.1.0")
+
+    def pageStart(self, partial):
+        pass
+
+    def pagePart(self, data):
+        pass
+
+    def pageEnd(self):
+        pass
+
+class Revision:
+    def __init__(self, repos, rev):
+        self.repos = repos
+        self.rev = rev
+        self.dirs_changed = []
+        self.author = None
+        self.log = None
+        self.date = None
+
+class StreamHandler(handler.ContentHandler):   
+    def __init__(self, bdec):
+        handler.ContentHandler.__init__(self) 
+        self.bdec =  bdec
+        self.rev = None
+        self.text_value = None
+
+    def startElement(self, name, attrs):
+        #print "start element: %s" % (name)
+        """
+        <commit repository="13f79535-47bb-0310-9956-ffa450edef68"
+                revision="815618">
+            <author>joehni</author>
+            <date>2009-09-16 06:00:21 +0000 (Wed, 16 Sep 2009)</date>
+            <log>pom.xml is not executable.</log>
+            <dirs_changed><path>commons/proper/commons-parent/trunk/</path></dirs_changed>
+        </commit>
+        """
+        if name == "commit":
+            self.rev = Revision(repos=attrs['repository'],
+                                rev=int(attrs['revision']))
+        elif name == "stillalive":
+            self.bdec.stillalive()
+    def characters(self, data):
+        if self.text_value is not None:
+            self.text_value = self.text_value + data 
+        else:
+            self.text_value = data
+
+    def endElement(self, name):
+        #print "end   element: %s" % (name)
+        if name == "commit":
+            self.bdec.commit(self.rev)
+            self.rev = None
+        if self.text_value is not None and self.rev is not None:
+            if name == "path":
+                self.rev.dirs_changed.append(self.text_value.strip())
+            if name == "author":
+                self.rev.author = self.text_value.strip()
+            if name == "date":
+                self.rev.date = self.text_value.strip()
+            if name == "log":
+                self.rev.log = self.text_value.strip()
+        self.text_value = None
+
+
+class XMLHTTPStream(HTTPStream):
+    def __init__(self, url, bdec):
+        HTTPStream.__init__(self, url)
+        self.bdec =  bdec
+        self.parser = make_parser(['xml.sax.expatreader'])
+        self.handler = StreamHandler(bdec)
+        self.parser.setContentHandler(self.handler)
+
+    def pageStart(self, parital):
+        self.bdec.pageStart()
+
+    def pagePart(self, data):
+        self.parser.feed(data)
+
+def connectTo(url, bdec):
+    u = urlparse(url)
+    port = u.port
+    if not port:
+        port = 80
+    s = XMLHTTPStream(url, bdec)
+    conn = reactor.connectTCP(u.hostname, u.port, s)
+    return [s, conn]
+
+
+CHECKBEAT_TIME = 90
+
+class BigDoEverythingClasss(object):
+    def __init__(self, config):
+        self.c = config
+        self.c._load_config()
+        self.url = str(self.c.config.get('stream'))
+        self.failures = 0
+        self.alive = time.time()
+        self.checker = task.LoopingCall(self._checkalive)
+        self.transport = None
+        self.stream = None
+        self._restartStream()
+        self.watch = []
+        self.twit = twitter.Twitter(self.c.config.get('username'), self.c.config.get('password'))
+
+    def pageStart(self):
+        log.msg("Stream Connection Established")
+        self.failures = 0
+        
+    def _restartStream(self):
+        (self.stream, self.transport) = connectTo(self.url, self)
+        self.stream.deferred.addBoth(self.streamDead)
+        self.alive = time.time()
+        self.checker.start(CHECKBEAT_TIME)
+
+    def _checkalive(self):
+        n = time.time()
+        if n - self.alive > CHECKBEAT_TIME:
+            log.msg("Stream is dead, reconnecting")
+            self.transport.disconnect()
+
+    def stillalive(self):
+        self.alive = time.time()
+
+    def streamDead(self, v):
+        BACKOFF_SECS = 5
+        BACKOFF_MAX = 60
+        self.checker.stop()
+
+        self.stream = None
+        self.failures += 1
+        backoff = min(self.failures * BACKOFF_SECS, BACKOFF_MAX)
+        log.msg("Stream disconnected, trying again in %d seconds.... %s" % (backoff, self.url))
+        reactor.callLater(backoff, self._restartStream)
+
+    def _normalize_path(self, path):
+        if path[0] != '/':
+            return "/" + path
+        return os.path.abspath(path)
+
+    def tweet(self, msg):
+        log.msg("SEND TWEET: %s" % (msg))
+        self.twit.update(msg).addCallback(self.tweet_done).addErrback(log.msg)
+
+    def tweet_done(self, x):
+        log.msg("TWEET: Success!")
+
+    def build_tweet(self, rev):
+        maxlen = 144
+        left = maxlen
+        paths = map(self._normalize_path, rev.dirs_changed)
+        if not len(paths):
+            return None
+        path = os.path.commonprefix(paths)
+        if path[0:1] == '/' and len(path) > 1:
+            path = path[1:]
+
+        #TODO: shorter link
+        link = " - http://svn.apache.org/viewvc?view=rev&revision=%d" % (rev.rev)
+        left -= len(link)
+        msg = "r%d in %s by %s: "  % (rev.rev, path, rev.author)
+        left -= len(msg)
+        if left > 3:
+            msg += rev.log[0:left]
+        msg += link
+        return msg
+
+    def commit(self, rev):
+        log.msg("COMMIT r%d (%d paths)" % (rev.rev, len(rev.dirs_changed)))
+        msg = self.build_tweet(rev)
+        if msg:
+            self.tweet(msg)
+            #print "Common Prefix: %s" % (pre)
+
+def main(config_file):
+    c = Config(config_file)
+    big = BigDoEverythingClasss(c)
+    reactor.run()
+
+if __name__ == "__main__":
+    if len(sys.argv) != 2:
+        print "invalid args, read source code"
+        sys.exit(0) 
+    log.startLogging(sys.stdout)
+    main(sys.argv[1])

Added: subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,466 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# SvnWcSub - Subscribe to a SvnPubSub stream, and keep a set of working copy
+# paths in sync
+#
+# Example:
+#  svnwcsub.py svnwcsub.conf
+#
+# On startup svnwcsub checks the working copy's path, runs a single svn update
+# and then watches for changes to that path.
+#
+# See svnwcsub.conf for more information on its contents.
+#
+
+import subprocess
+import threading
+import sys
+import os
+import re
+import ConfigParser
+import time
+
+from twisted.internet import defer, reactor, task, threads
+from twisted.internet.utils import getProcessOutput
+from twisted.application import internet
+from twisted.python import failure, log
+from twisted.web.client import HTTPClientFactory, HTTPPageDownloader
+from urlparse import urlparse
+from xml.sax import handler, make_parser
+from twisted.internet import protocol
+
+
+"""
+Wrapper around svn(1), just to keep it from spreading everywhere, incase
+we ever convert to another python-subversion bridge api.  (This has happened;
+this class used to wrap pysvn.)
+
+Yes, this exposes accessors for each piece of info we need, but it keeps it
+simpler.
+"""
+class SvnClient(object):
+    def __init__(self, path, url):
+        self.path = path
+        self.url = url
+        self.info = {}
+
+    def cleanup(self):
+        # despite the name, this just deletes the client context ---
+        # which is a no-op for svn(1) wrappers.
+        pass
+
+    def _run_info(self):
+        "run `svn info` and return the output"
+        argv = ["svn", "info", "--non-interactive", "--", self.path]
+        output = None
+
+        if not os.path.isdir(self.path):
+            log.msg("autopopulate %s from %s" % ( self.path, self.url))
+            subprocess.check_call(['svn', 'co', '-q', '--non-interactive', '--config-dir', '/home/svnwc/.subversion', '--', self.url, self.path])
+
+        if hasattr(subprocess, 'check_output'):
+            output = subprocess.check_output(argv)
+        else:
+            pipe = subprocess.Popen(argv, stdout=subprocess.PIPE)
+            output, _ = pipe.communicate()
+            if pipe.returncode:
+                raise subprocess.CalledProcessError(pipe.returncode, argv)
+        return output
+  
+    def _get_info(self, force=False):
+        "run `svn info` and parse that info self.info"
+        if force or not self.info:
+            info = {}
+            for line in self._run_info().split("\n"):
+                # Ensure there's at least one colon-space in the line, to avoid
+                # unpack errors.
+                name, value = ("%s: " % line).split(': ', 1)
+                # Canonicalize the key names.
+                info[{
+                  "Repository Root": 'repos',
+                  "URL": 'url',
+                  "Repository UUID": 'uuid',
+                  "Revision": 'revision',
+                }.get(name, None)] = value[:-2] # unadd the colon-space
+            self.info = info
+
+    def get_repos(self):
+        self._get_info()
+        return unicode(self.info['repos'])
+
+    def get_url(self):
+        self._get_info()
+        return unicode(self.info['url'])
+
+    def get_uuid(self):
+        self._get_info()
+        return unicode(self.info['uuid'])
+
+    def update(self):
+        subprocess.check_call(
+            ["svn", "update", "--non-interactive", "-q", "--", self.path]
+        )
+        self._get_info(True)
+        return int(self.info['revision'])
+
+    # TODO: Wrap status
+    def status(self):
+        return None
+
+"""This has been historically implemented via svn(1) even when SvnClient
+used pysvn."""
+class ProcSvnClient(SvnClient):
+  def __init__(self, path, svnbin="svn", env=None, url=None):
+    super(ProcSvnClient, self).__init__(path, url)
+    self.svnbin = svnbin
+    self.env = env
+
+  @defer.inlineCallbacks
+  def update(self):
+    # removed since this breaks when the SSL certificate names are mismatched, even
+    # if we marked them as trust worthy
+    # '--trust-server-cert', 
+    cmd = [self.svnbin, '--config-dir', '/home/svnwc/.subversion', '--trust-server-cert', '--non-interactive', 'cleanup', self.path]
+    output = yield getProcessOutput(cmd[0], args=cmd[1:], env=self.env)
+    cmd = [self.svnbin, '--config-dir', '/home/svnwc/.subversion', '--trust-server-cert', '--non-interactive', 'update', '--ignore-externals', self.path]
+    output = yield getProcessOutput(cmd[0], args=cmd[1:], env=self.env)
+    rev = int(output[output.rfind("revision ")+len("revision "):].replace('.', ''))
+    defer.returnValue(rev)
+
+class WorkingCopy(object):
+    def __init__(self, bdec, path, url):
+        self.lock = threading.Lock()
+        self.bdec = bdec
+        self.path = path
+        self.url = url
+        self.repos = None
+        self.match = None
+        d = threads.deferToThread(self._get_match)
+        d.addCallback(self._set_match)
+
+    def _set_match(self, value):
+        self.match = str(value[0])
+        self.url = value[1]
+        self.repos = value[2]
+        self.uuid = value[3]
+        self.bdec.wc_ready(self)
+
+    def update_applies(self, uuid, path):
+        if self.uuid != uuid:
+            return False
+
+        path = str(path)
+        if path == self.match:
+            #print "ua: Simple match"
+            # easy case. woo.
+            return True
+        if len(path) < len(self.match):
+            # path is potentially a parent directory of match?
+            #print "ua: parent check"
+            if self.match[0:len(path)] == path:
+                return True
+        if len(path) > len(self.match):
+            # path is potentially a sub directory of match
+            #print "ua: sub dir check"
+            if path[0:len(self.match)] == self.match:
+                return True
+        return False
+
+    @defer.inlineCallbacks
+    def update(self):
+        c = ProcSvnClient(self.path, self.bdec.svnbin, self.bdec.env, self.url)
+        rev = yield c.update()
+        c.cleanup()
+        defer.returnValue(rev)
+#        return threads.deferToThread(self._update)
+
+    def _update(self):
+        self.lock.acquire()
+        try:
+            c = ProcSvnClient(self.path, self.bdec.svnbin, self.bdec.env, self.url)
+            rev = c.update()
+            c.cleanup()
+            return rev
+        finally:
+            self.lock.release()
+
+    def _get_match(self):
+        self.lock.acquire()
+        try:
+            c = SvnClient(self.path, self.url)
+            repos = c.get_repos()
+            url = c.get_url()
+            uuid = c.get_uuid()
+            match  = url[len(repos):]
+            c.cleanup()
+            return [match, url, repos, uuid]
+        finally:
+            self.lock.release()
+        
+
+class HTTPStream(HTTPClientFactory):
+    protocol = HTTPPageDownloader
+
+    def __init__(self, url):
+        self.url = url
+        HTTPClientFactory.__init__(self, url, method="GET", agent="SvnWcSub/0.1.0")
+
+    def pageStart(self, partial):
+        pass
+
+    def pagePart(self, data):
+        pass
+
+    def pageEnd(self):
+        pass
+
+class Revision:
+    def __init__(self, repos, rev):
+        self.repos = repos
+        self.rev = rev
+        self.dirs_changed = []
+
+class StreamHandler(handler.ContentHandler):   
+    def __init__(self, stream, bdec):
+        handler.ContentHandler.__init__(self) 
+        self.stream = stream
+        self.bdec =  bdec
+        self.rev = None
+        self.text_value = None
+
+    def startElement(self, name, attrs):
+        #print "start element: %s" % (name)
+        """
+        <commit revision="7">
+                        <dirs_changed><path>/</path></dirs_changed>
+                      </commit> 
+        """
+        if name == "commit":
+            self.rev = Revision(attrs['repository'], int(attrs['revision']))
+        elif name == "stillalive":
+            self.bdec.stillalive(self.stream)
+    def characters(self, data):
+        if self.text_value is not None:
+            self.text_value = self.text_value + data 
+        else:
+            self.text_value = data
+
+    def endElement(self, name):
+        #print "end   element: %s" % (name)
+        if name == "commit":
+            self.bdec.commit(self.stream, self.rev)
+            self.rev = None
+        if name == "path" and self.text_value is not None and self.rev is not None:
+            self.rev.dirs_changed.append(self.text_value.strip())
+        self.text_value = None
+
+
+class XMLHTTPStream(HTTPStream):
+    def __init__(self, url, bdec):
+        HTTPStream.__init__(self, url)
+        self.alive = 0
+        self.bdec =  bdec
+        self.parser = make_parser(['xml.sax.expatreader'])
+        self.handler = StreamHandler(self, bdec)
+        self.parser.setContentHandler(self.handler)
+
+    def pageStart(self, parital):
+        self.bdec.pageStart(self)
+
+    def pagePart(self, data):
+        self.parser.feed(data)
+
+    def pageEnd(self):
+        self.bdec.pageEnd(self)
+
+def connectTo(url, bdec):
+    u = urlparse(url)
+    port = u.port
+    if not port:
+        port = 80
+    s = XMLHTTPStream(url, bdec)
+    if bdec.service:
+      conn = internet.TCPClient(u.hostname, u.port, s)
+      conn.setServiceParent(bdec.service)
+    else:
+      conn = reactor.connectTCP(u.hostname, u.port, s)
+    return [s, conn]
+
+
+CHECKBEAT_TIME = 60
+PRODUCTION_RE_FILTER = re.compile("/websites/production/[^/]+/")
+
+class BigDoEverythingClasss(object):
+    def __init__(self, config, service = None):
+        self.urls = [s.strip() for s in config.get_value('streams').split()]
+        self.svnbin = config.get_value('svnbin')
+        self.env = config.get_env()
+        self.service = service
+        self.failures = 0
+        self.alive = time.time()
+        self.checker = task.LoopingCall(self._checkalive)
+        self.transports = {}
+        self.streams = {}
+        for u in self.urls:
+          self._restartStream(u)
+        self.watch = []
+        for path, url in config.get_track().items():
+            # working copies auto-register with the BDEC when they are ready.
+            WorkingCopy(self, path, url)
+        self.checker.start(CHECKBEAT_TIME)
+
+    def pageStart(self, stream):
+        log.msg("Stream %s Connection Established" % (stream.url))
+        self.failures = 0
+
+    def pageEnd(self, stream):
+        log.msg("Stream %s Connection Dead" % (stream.url))
+        self.streamDead(stream.url)
+
+    def _restartStream(self, url):
+        (self.streams[url], self.transports[url]) = connectTo(url, self)
+        self.streams[url].deferred.addBoth(self.streamDead, url)
+        self.streams[url].alive = time.time()
+
+    def _checkalive(self):
+        n = time.time()
+        for k in self.streams.keys():
+          s = self.streams[k]
+          if n - s.alive > CHECKBEAT_TIME:
+            log.msg("Stream %s is dead, reconnecting" % (s.url))
+            #self.transports[s.url].disconnect()
+            self.streamDead(self, s.url)
+
+#        d=filter(lambda x:x not in self.streams.keys(), self.urls)
+#        for u in d:
+#          self._restartStream(u)
+
+    def stillalive(self, stream):
+        stream.alive = time.time()
+
+    def streamDead(self, url, result=None):
+        s = self.streams.get(url)
+        if not s:
+          log.msg("Stream %s is messed up" % (url))
+          return
+        BACKOFF_SECS = 5
+        BACKOFF_MAX = 60
+        #self.checker.stop()
+
+        self.streams[url] = None
+        self.transports[url] = None
+        self.failures += 1
+        backoff = min(self.failures * BACKOFF_SECS, BACKOFF_MAX)
+        log.msg("Stream disconnected, trying again in %d seconds.... %s" % (backoff, s.url))
+        reactor.callLater(backoff, self._restartStream, url)
+
+    @defer.inlineCallbacks
+    def wc_ready(self, wc):
+        # called when a working copy object has its basic info/url,
+        # Add it to our watchers, and trigger an svn update.
+        log.msg("Watching WC at %s <-> %s" % (wc.path, wc.url))
+        self.watch.append(wc)
+        rev = yield wc.update()
+        log.msg("wc update: %s is at r%d" % (wc.path, rev))
+
+    def _normalize_path(self, path):
+        if path[0] != '/':
+            return "/" + path
+        return os.path.abspath(path)
+
+    @defer.inlineCallbacks
+    def commit(self, stream, rev):
+        log.msg("COMMIT r%d (%d paths) via %s" % (rev.rev, len(rev.dirs_changed), stream.url))
+        paths = map(self._normalize_path, rev.dirs_changed)
+        if len(paths):
+            pre = os.path.commonprefix(paths)
+            if pre == "/websites/":
+                # special case for svnmucc "dynamic content" buildbot commits
+                # just take the first production path to avoid updating all cms working copies
+                for p in paths:
+                    m = PRODUCTION_RE_FILTER.match(p)
+                    if m:
+                        pre = m.group(0)
+                        break
+
+            #print "Common Prefix: %s" % (pre)
+            wcs = [wc for wc in self.watch if wc.update_applies(rev.repos, pre)]
+            log.msg("Updating %d WC for r%d" % (len(wcs), rev.rev))
+            for wc in wcs:
+                rev = yield wc.update()
+                log.msg("wc update: %s is at r%d" % (wc.path, rev))
+
+
+class ReloadableConfig(ConfigParser.SafeConfigParser):
+    def __init__(self, fname):
+        ConfigParser.SafeConfigParser.__init__(self)
+
+        self.fname = fname
+        self.read(fname)
+
+        ### install a signal handler to set SHOULD_RELOAD. BDEC should
+        ### poll this flag, and then adjust its internal structures after
+        ### the reload.
+        self.should_reload = False
+
+    def reload(self):
+        # Delete everything. Just re-reading would overlay, and would not
+        # remove sections/options. Note that [DEFAULT] will not be removed.
+        for section in self.sections():
+            self.remove_section(section)
+
+        # Now re-read the configuration file.
+        self.read(fname)
+
+    def get_value(self, which):
+        return self.get(ConfigParser.DEFAULTSECT, which)
+
+    def get_env(self):
+        env = os.environ.copy()
+        default_options = self.defaults().keys()
+        for name, value in self.items('env'):
+            print name, value
+            if name not in default_options:
+                env[name] = value
+        return env
+
+    def get_track(self):
+        "Return the {PATH: URL} dictionary of working copies to track."
+        track = dict(self.items('track'))
+        for name in self.defaults().keys():
+            del track[name]
+        return track
+
+    def optionxform(self, option):
+        # Do not lowercase the option name.
+        return str(option)
+
+
+def main(config_file):
+    c = ReloadableConfig(config_file)
+    big = BigDoEverythingClasss(c)
+    reactor.run()
+
+if __name__ == "__main__":
+    if len(sys.argv) != 2:
+        print "invalid args, read source code"
+        sys.exit(0) 
+    log.startLogging(sys.stdout)
+    main(sys.argv[1])

Propchange: subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/test.conf
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/test.conf?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/test.conf (added)
+++ subversion/trunk/tools/server-side/svnpubsub/test.conf Tue Feb 28 18:52:13 2012
@@ -0,0 +1,4 @@
+# For use with testserver.py
+
+[DEFAULT]
+streams: http://127.0.0.1:2069/commits/xml

Added: subversion/trunk/tools/server-side/svnpubsub/testserver.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/testserver.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/testserver.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/testserver.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# A simple test server for responding in different ways to SvnPubSub clients.
+# This avoids the complexity of the Twisted framework in order to direct
+# various (abnormal) conditions at the client.
+#
+# ### usage...
+#
+
+import sys
+import BaseHTTPServer
+
+
+PORT = 2069
+
+TEST_BODY = '<commit repository="12345678-1234-1234-1234-123456789012" revision="1234"><author>johndoe</author><date>2012-01-01 01:01:01 +0000 (Sun, 01 Jan 2012)</date><log>Frob the ganoozle with the snookish</log><dirs_changed><path>one/path/</path><path>some/other/directory/</path></dirs_changed></commit>'
+
+SEND_KEEPALIVE = True
+
+
+class TestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+  def do_GET(self):
+    self.send_response(200)
+    self.send_header('Content-Length', str(len(TEST_BODY)))
+    self.send_header('Connection', 'keep-alive')
+    self.end_headers()
+    self.wfile.write(TEST_BODY)
+
+
+if __name__ == '__main__':
+  server = BaseHTTPServer.HTTPServer(('', PORT), TestHandler)
+  sys.stderr.write('Now listening on port %d...\n' % (PORT,))
+  server.serve_forever()

Propchange: subversion/trunk/tools/server-side/svnpubsub/testserver.py
------------------------------------------------------------------------------
    svn:executable = *

Added: subversion/trunk/tools/server-side/svnpubsub/watcher.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/watcher.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/watcher.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/watcher.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Watch for events from SvnPubSub and print them to stdout
+#
+# ### usage...
+#
+
+import sys
+import urlparse
+import pprint
+
+import svnpubsub.client
+import svnwcsub  ### for ReloadableConfig
+
+
+def _commit(host, port, rev):
+  print 'COMMIT: from %s:%s' % (host, port)
+  pprint.pprint(vars(rev), indent=2)
+
+
+def _event(host, port, event_name):
+  print 'EVENT: from %s:%s "%s"' % (host, port, event_name)
+
+
+def main(config_file):
+  config = svnwcsub.ReloadableConfig(config_file)
+  hostports = [ ]
+  for url in config.get_value('streams').split():
+    parsed = urlparse.urlparse(url)
+    hostports.append((parsed.hostname, parsed.port))
+
+  mc = svnpubsub.client.MultiClient(hostports, _commit, _event)
+  mc.run_forever()
+
+
+if __name__ == "__main__":
+  if len(sys.argv) != 2:
+    print "invalid args, read source code"
+    sys.exit(0) 
+  main(sys.argv[1])

Propchange: subversion/trunk/tools/server-side/svnpubsub/watcher.py
------------------------------------------------------------------------------
    svn:executable = *