Import of Buildbot 0.7.9 tarball. BUILDBOT_0_7_9
authorBen Hearsum <bhearsum@mozilla.com>
Mon, 26 Jan 2009 13:22:12 -0500
changeset 0 9e5c462732f9cfe3c0228845e81065c3c2d94e00
child 1 8a4c35ada8f2c311d5125402968a93258b60d5b2
child 14 54f7d8c5c4e8278a1294eb9b332e197a57da881f
push id1
push userbhearsum@mozilla.com
push dateTue, 27 Jan 2009 19:34:08 +0000
Import of Buildbot 0.7.9 tarball.
COPYING
CREDITS
MANIFEST.in
NEWS
PKG-INFO
README
README.w32
bin/buildbot
buildbot.egg-info/PKG-INFO
buildbot.egg-info/SOURCES.txt
buildbot.egg-info/dependency_links.txt
buildbot.egg-info/requires.txt
buildbot.egg-info/top_level.txt
buildbot/__init__.py
buildbot/buildbot.png
buildbot/buildset.py
buildbot/buildslave.py
buildbot/changes/__init__.py
buildbot/changes/base.py
buildbot/changes/bonsaipoller.py
buildbot/changes/changes.py
buildbot/changes/dnotify.py
buildbot/changes/freshcvs.py
buildbot/changes/hgbuildbot.py
buildbot/changes/mail.py
buildbot/changes/maildir.py
buildbot/changes/monotone.py
buildbot/changes/p4poller.py
buildbot/changes/pb.py
buildbot/changes/svnpoller.py
buildbot/clients/__init__.py
buildbot/clients/base.py
buildbot/clients/debug.glade
buildbot/clients/debug.py
buildbot/clients/gtkPanes.py
buildbot/clients/sendchange.py
buildbot/dnotify.py
buildbot/interfaces.py
buildbot/locks.py
buildbot/manhole.py
buildbot/master.py
buildbot/pbutil.py
buildbot/process/__init__.py
buildbot/process/base.py
buildbot/process/builder.py
buildbot/process/buildstep.py
buildbot/process/factory.py
buildbot/process/process_twisted.py
buildbot/process/properties.py
buildbot/process/step_twisted2.py
buildbot/scheduler.py
buildbot/scripts/__init__.py
buildbot/scripts/checkconfig.py
buildbot/scripts/logwatcher.py
buildbot/scripts/reconfig.py
buildbot/scripts/runner.py
buildbot/scripts/sample.cfg
buildbot/scripts/startup.py
buildbot/scripts/tryclient.py
buildbot/slave/__init__.py
buildbot/slave/bot.py
buildbot/slave/commands.py
buildbot/slave/interfaces.py
buildbot/slave/registry.py
buildbot/sourcestamp.py
buildbot/status/__init__.py
buildbot/status/base.py
buildbot/status/builder.py
buildbot/status/client.py
buildbot/status/html.py
buildbot/status/mail.py
buildbot/status/progress.py
buildbot/status/tests.py
buildbot/status/tinderbox.py
buildbot/status/web/__init__.py
buildbot/status/web/about.py
buildbot/status/web/base.py
buildbot/status/web/baseweb.py
buildbot/status/web/build.py
buildbot/status/web/builder.py
buildbot/status/web/changes.py
buildbot/status/web/classic.css
buildbot/status/web/grid.py
buildbot/status/web/index.html
buildbot/status/web/logs.py
buildbot/status/web/robots.txt
buildbot/status/web/slaves.py
buildbot/status/web/step.py
buildbot/status/web/tests.py
buildbot/status/web/waterfall.py
buildbot/status/web/xmlrpc.py
buildbot/status/words.py
buildbot/steps/__init__.py
buildbot/steps/dummy.py
buildbot/steps/maxq.py
buildbot/steps/python.py
buildbot/steps/python_twisted.py
buildbot/steps/shell.py
buildbot/steps/source.py
buildbot/steps/transfer.py
buildbot/steps/trigger.py
buildbot/test/__init__.py
buildbot/test/emit.py
buildbot/test/emitlogs.py
buildbot/test/mail/freshcvs.1
buildbot/test/mail/freshcvs.2
buildbot/test/mail/freshcvs.3
buildbot/test/mail/freshcvs.4
buildbot/test/mail/freshcvs.5
buildbot/test/mail/freshcvs.6
buildbot/test/mail/freshcvs.7
buildbot/test/mail/freshcvs.8
buildbot/test/mail/freshcvs.9
buildbot/test/mail/svn-commit.1
buildbot/test/mail/svn-commit.2
buildbot/test/mail/syncmail.1
buildbot/test/mail/syncmail.2
buildbot/test/mail/syncmail.3
buildbot/test/mail/syncmail.4
buildbot/test/mail/syncmail.5
buildbot/test/runutils.py
buildbot/test/sleep.py
buildbot/test/subdir/emit.py
buildbot/test/test__versions.py
buildbot/test/test_bonsaipoller.py
buildbot/test/test_buildreq.py
buildbot/test/test_buildstep.py
buildbot/test/test_changes.py
buildbot/test/test_config.py
buildbot/test/test_control.py
buildbot/test/test_dependencies.py
buildbot/test/test_locks.py
buildbot/test/test_maildir.py
buildbot/test/test_mailparse.py
buildbot/test/test_p4poller.py
buildbot/test/test_properties.py
buildbot/test/test_run.py
buildbot/test/test_runner.py
buildbot/test/test_scheduler.py
buildbot/test/test_shell.py
buildbot/test/test_slavecommand.py
buildbot/test/test_slaves.py
buildbot/test/test_status.py
buildbot/test/test_steps.py
buildbot/test/test_svnpoller.py
buildbot/test/test_transfer.py
buildbot/test/test_twisted.py
buildbot/test/test_util.py
buildbot/test/test_vc.py
buildbot/test/test_web.py
buildbot/test/test_webparts.py
buildbot/util.py
contrib/CSS/sample1.css
contrib/CSS/sample2.css
contrib/OS-X/README
contrib/OS-X/net.sourceforge.buildbot.master.plist
contrib/OS-X/net.sourceforge.buildbot.slave.plist
contrib/README.txt
contrib/arch_buildbot.py
contrib/bb_applet.py
contrib/darcs_buildbot.py
contrib/fakechange.py
contrib/git_buildbot.py
contrib/hg_buildbot.py
contrib/run_maxq.py
contrib/svn_buildbot.py
contrib/svn_watcher.py
contrib/svnpoller.py
contrib/viewcvspoll.py
contrib/windows/buildbot.bat
contrib/windows/buildbot2.bat
contrib/windows/buildbot_service.py
contrib/windows/setup.py
docs/buildbot.html
docs/buildbot.info
docs/buildbot.info-1
docs/buildbot.info-2
docs/buildbot.texinfo
docs/epyrun
docs/examples/hello.cfg
docs/examples/twisted_master.cfg
docs/gen-reference
docs/hexnut32.png
docs/hexnut48.png
docs/hexnut64.png
docs/images/master.png
docs/images/master.svg
docs/images/master.txt
docs/images/overview.png
docs/images/overview.svg
docs/images/overview.txt
docs/images/slavebuilder.png
docs/images/slavebuilder.svg
docs/images/slavebuilder.txt
docs/images/slaves.png
docs/images/slaves.svg
docs/images/slaves.txt
docs/images/status.png
docs/images/status.svg
docs/images/status.txt
setup.cfg
setup.py
new file mode 100644
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
new file mode 100644
--- /dev/null
+++ b/CREDITS
@@ -0,0 +1,74 @@
+This is a list of everybody who has contributed to Buildbot in some way, in
+no particular order. Thanks everybody!
+
+Aaron Hsieh
+Albert Hofkamp
+Alexander Lorenz
+Alexander Staubo
+AllMyData.com
+Andrew Bennetts
+Anthony Baxter
+Baptiste Lepilleur
+Bear
+Ben Hearsum
+Benoit Sigoure
+Brad Hards
+Brandon Philips
+Brett Neely
+Charles Lepple
+Christian Unger
+Clement Stenac
+Dan Locks
+Dave Liebreich
+Dave Peticolas
+Dobes Vandermeer
+Dustin Mitchell
+Elliot Murphy
+Fabrice Crestois
+Gary Granger
+Gerald Combs
+Greg Ward
+Grig Gheorghiu
+Haavard Skinnemoen
+JP Calderone
+James Knight
+Jerome Davann
+John Backstrand
+John O'Duinn
+John Pye
+John Saxton
+Jose Dapena Paz
+Kevin Turner
+Kirill Lapshin
+Marius Gedminas
+Mark Dillavou
+Mark Hammond
+Mark Pauley
+Mark Rowe
+Mateusz Loskot
+Nathaniel Smith
+Neal Norwitz
+Nick Mathewson
+Nick Trout
+Niklaus Giger
+Olivier Bonnet
+Olly Betts
+Paul Warren
+Paul Winkler
+Phil Thompson
+Philipp Frauenfelder
+Rene Rivera
+Riccardo Magliocchetti
+Rob Helmer
+Roch Gadsdon
+Roy Rapoport
+Scott Lamb
+Stephen Davis
+Steven Walter
+Ted Mielczarek
+Thomas Vander Stichele
+Tobi Vollebregt
+Wade Brainerd
+Yoz Grahame
+Zandr Milewski
+chops
new file mode 100644
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,14 @@
+
+include MANIFEST.in README README.w32 NEWS CREDITS COPYING
+include docs/examples/*.cfg
+include docs/buildbot.texinfo docs/buildbot.info* docs/buildbot.html
+include docs/*.png docs/images/*.png docs/images/*.svg docs/images/*.txt
+include docs/epyrun docs/gen-reference
+include buildbot/test/mail/* buildbot/test/subdir/*
+include buildbot/scripts/sample.cfg
+include buildbot/status/web/*.css buildbot/status/web/*.html
+include buildbot/status/web/robots.txt
+include buildbot/clients/debug.glade
+include buildbot/buildbot.png
+
+include contrib/* contrib/windows/* contrib/OS-X/* contrib/CSS/*
new file mode 100644
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,2406 @@
+User visible changes in Buildbot.             -*- outline -*-
+
+* Release 0.7.9 (15 Sep 2008)
+
+** New Features
+
+*** Configurable public_html directory (#162)
+
+The public_html/ directory, which provides static content for the WebStatus()
+HTTP server, is now configurable. The default location is still the
+public_html/ subdirectory of the buildmaster's base directory, but you can
+change this by passing a suitable argument when creating the WebStatus()
+instance in your master.cfg file:
+
+  c['status'].append( WebStatus(8080, public_html="/var/www/buildbot") )
+
+*** Lock access modes (#313)
+
+Albert Hofkamp added code to provide two distinct access modes to Locks:
+"counting" and "exclusive". Locks can accept a configurable number of
+"counting"-mode users, or a single "exclusive"-mode. For example, a Lock is
+defined with maxCount=3, and then a 'compile' BuildStep uses this lock in
+counting mode, while a 'cleanup' BuildStep uses this lock in exclusive mode.
+Then, there can be one, two, or three simultaneous Builds in the compile step
+(as long as there are no builds in the cleanup step). Only one build can be
+in the cleanup step at a time, and if there is such a build in the cleanup
+step, then the compile steps in other builds will wait for it to finish.
+Please see the "Interlocks" section of the user's manual for more details.
+
+** Bugs Fixed
+
+*** Buildslave missing_timeout= fired too quickly (#211)
+
+By providing a missing_timeout= argument when creating the BuildSlave
+instance, you can ask the buildmaster to send email if a buildslave is
+disconnected for too long. A bug in the previous version caused this
+notification to be sent too soon, rather than waiting until the timeout
+period expired. This should be fixed now.
+
+*** Test command display fixed (#332)
+
+In the previous version, a steps.shell.Test step would display the parsed
+test results (in the step's box on the waterfall display) in lieu of any
+other descriptive text the step might provide. In this release, these two
+pieces of information are combined.
+
+** Minor Changes
+
+The buildmaster's version is logged to its twistd.log file at startup. The
+buildslave does the same, to its own logfile.
+
+Remote commands now record how long each command took. The "elapsedTime="
+message will appear in the step's main logfile.
+
+The "buildbot restart" command no longer fails if the buildbot wasn't already
+running.
+
+The FileUpload and FileDownload steps now create their target directories
+(and any missing intermediate directories) before writing to the destination
+file.
+
+The per-build and per-step web pages now show the start, finish, and elapsed
+time of their build or step.
+
+If a Subversion-based build is started with a mixture of Changes that specify
+particular numeric revisions and "HEAD" Changes (which indicate that a trunk
+checkout is desired), the build will use a trunk checkout. Previously this
+would probably cause an error. It is not clear how this situation might
+arise.
+
+** Compability With Other Tools
+
+The mercurial commit hook (buildbot.changes.hgbuildbot) in the previous
+version doesn't work with hg-1.0 or later (it uses an API function that was
+present in the hg-0.9.5 release, but was removed from hg-1.0). This
+incompability has been fixed: the new version of buildbot should be
+compatible with hg-1.0 and newer (and it probably retains compability with
+hg-0.9.5 and earlier too). (#328)
+
+The Git tool has traditionally provided two ways to run each command, either
+as subcommands of /usr/bin/git (like "git checkout"), or as individual tools
+(like /usr/bin/git-checkout). The latter form is being removed in the
+upcoming 1.6 Git release. Previous versions of Buildbot have used the
+git-checkout form, and will break when Git is upgraded to 1.6 or beyond. The
+new Buildbot release switches to the subcommand form. Note that this is a
+change on the buildslave side.
+
+The Git checkout command will now use the default branch (as set in the
+steps.source.Git() step definition) if the changes that it is building do not
+specify some other branch to build. (#340)
+
+** Deprecation Schedule
+
+No features have been deprecated in this release, and no deprecated features
+have been removed. As a reminder, the following deprecated features are
+scheduled for removal in an upcoming release:
+
+c['sources'] (plural) was replaced by c['change_source'] (singular) in 0.7.6,
+and will be removed by 0.8.0.
+
+c['bots'] was replaced by c['buildslaves'] in 0.7.6, and will be removed by
+0.8.0 . c['bots'] only accepts BuildSlave instances, not name/passwd tuples.
+
+The html.Waterfall status target was replaced by html.WebStatus in 0.7.6, and
+will be removed by 0.8.0.
+
+
+* Release 0.7.8 (24 Jul 2008)
+
+** New features
+
+The IRC bot will respond to three new commands: 'notify' subscribes the
+channel (or the sender, if the command is sent as a private "/msg") to hear
+about build events. 'join' tells the bot to join some new IRC channel.
+'leave' tells it to leave a channel. See the "IRC Bot" section of the User's
+Manual for details. (#171)
+
+Build Steps now have "statistics", in addition to logfiles. These are used to
+count things like how many tests passed or failed. There are methods to sum
+these counters across all steps and display the results in the Build status.
+The Waterfall display now shows the count of failed tests on the top-most box
+in each column, using this mechanism.
+
+The new buildbot.steps.shell.PerlModuleTest step was added, to run Perl unit
+tests. This is a wrapper around the regular ShellCommand that parses the
+output of the standard perl unit test system and counts how many tests
+passed/failed/etc. The results are put into the step's summary text, and a
+count of tests passed/failed/skipped are tracked in the steps's statistics.
+The factory.CPAN build factory has been updated to use this, so configuring a
+Buildbot to test a perl module available from CPAN should be as easy as:
+
+ s = source.CVS(cvsroot, cvsmodule)
+ f = factory.CPAN(s)
+
+Build Properties have been generalized: they remain associated with a single
+Build, but the properties can be set from a variety of sources. In previous
+releases, the Build itself would set properties like 'buildername', 'branch',
+and 'revision' (the latter two indicating which version of the source code it
+was trying to get), and the source-checkout BuildSteps would set a property
+named 'got_revision' (to indicate what version of the soruce code it actually
+got). In this release, the 'scheduler' property is set to indicate which
+Scheduler caused the build to be started. In addition, the config file can
+specify properties to be set on all Builds, or on all Builds for a specific
+Builder. All these properties are available for interpolation into
+ShellCommands and environment variables by using the WithProperties() marker.
+
+It may be easier to implement simple build parameterization (e.g. to upload
+generated binaries to a specific directory, or to only perform long-running
+tests on a nightly build instead of upon every checkin) by using these Build
+Properties than to write custom BuildSteps.
+
+** Other improvements
+
+The /buildslaves web page shows which slaves are currently running builds.
+Offline slaves are displayed in bold.
+
+Buildbot's setup.py now provides metadata to setuptools (if installed): an
+entry_points script was added, and a dependency upon twisted-2.4.x or newer
+was declared. This makes it more likely that 'easy_install buildbot' will
+work.
+
+The MailNotifier class acquired a mode="passing" flag: in this mode, the
+buildbot will only send mail about passing builds (versus only on failing
+builds, or only on builds which failed when the previous build had passed).
+
+** Bugs fixed
+
+Don't display force/stop build buttons when build control is disabled (#246)
+
+When a build is waiting on a lock, don't claim that it has started (#107)
+
+Make SVN mode=copy tolerate symlinks on freebsd, "cp -rp" -> "cp -RPp" (#86)
+
+The svnpoller changesource now ignores branch deletion (#261)
+
+The Git unit tests should run even if the user has not told Git about their
+username/email.
+
+The WebStatus /xmlrpc server's getStatus() method was renamed to the
+more-accurate getLastBuildResults().
+
+The TinderboxMailNotifier status output acquired an useChangeTime= argument.
+
+The bonsaipoller changesource got some fixes.
+
+** Deprecation Schedule
+
+No features have been deprecated in this release, and no deprecated features
+have been removed. As a reminder, the following deprecated features are
+scheduled for removal in an upcoming release:
+
+c['sources'] (plural) was replaced by c['change_source'] (singular) in 0.7.6,
+and will be removed by 0.8.0.
+
+c['bots'] was replaced by c['buildslaves'] in 0.7.6, and will be removed by
+0.8.0 . c['bots'] only accepts BuildSlave instances, not name/passwd tuples.
+
+The html.Waterfall status target was replaced by html.WebStatus in 0.7.6, and
+will be removed by 0.8.0.
+
+
+
+* Release 0.7.7 (29 Mar 2008)
+
+** Things You Need To Know
+
+*** builder names must not start with an underscore (`_').
+
+These are now reserved for internal buildbot purposes, such as the magic
+"_all" pseudo-builder that the web pages use to allow force-build buttons
+that start builds on all Builders at once.
+
+** New Features
+
+*** "buildbot checkconfig"
+
+The "buildbot checkconfig" command will look at your master.cfg file and tell
+you if there are any problems with it. This can be used to test potential
+changes to your config file before submitting them to the running
+buildmaster. This is particularly useful to run just before doing "buildbot
+restart", since the restart will fail if the config file has an error. By
+running "buildbot checkconfig master.cfg && buildbot restart", you'll only
+perform the restart if the config file was ok. Many thanks to Ben Hearsum for
+the patch.
+
+*** Waterfall "?category=FOO" query-arguments
+
+The Waterfall page now accepts one or more "category=" query arguments in the
+URL, to filter the display by categories. These behave a lot like the
+"builder=" query argument. Thanks to Jermo Davann for the patch.
+
+** Bugs Fixed
+
+Many bugs were fixed, and many minor features were added. Many thanks to
+Dustin Mitchell who fixed and coordinated many of these. Here is a terse
+list, for more details, please see the Trac page for the 0.7.7 release, at
+http://buildbot.net/trac/query?status=closed&milestone=0.7.7 :
+
+Many of the URLs generated by the buildbot were wrong.
+Display of last-heard-from timestamps on the buildslaves web page were wrong.
+Asking an IRC bot about a build waiting on a Lock should no longer crash.
+Same for the web viewer.
+Stop treating the encouraged info/ directory as leftover.
+Add more force/stop build buttons.
+Timestamps displayed on the waterfall now handle daylight savings properly.
+p4poller no longer quits after a single failure.
+Improved Git support, including 'try', branch, and revisions.
+Buildslaves now use 'git', not 'cogito'.
+Make older hg client/servers handle specific-revision builds properly.
+Fix twisted.scripts._twistw problem on twisted-2.5.0 and windows.
+Fix workdir= and env= on ShellCommands
+Fix logfile-watching in 'buildbot start' on OS-X.
+Fix ShellCommand crashes when the program emits >640kB of output per chunk.
+New WarningCountingShellCommand step.
+Fix TreeSize step.
+Fix transfer.FileUpload/FileDownload crashes for large files.
+Make 'buildbor reconfig' on windows tell you that it doesn't work.
+Add a To: header to the mail sent by the slave-missing timeout.
+Disable usePTY= for most unit tests, it makes some debian systems flunk tests.
+Add 'absolute source stamps'
+Add 'triggerable schedulers', and a buildstep to trigger them.
+Remove buildbot.changes.freshcvsmail
+Add new XMLRPC methods: getAllBuilders, getStatus, getLastBuilds.
+Accept WithProperties in more places: env=, workdir=, others.
+Use --no-auth-cache with SVN commands to avoid clobbering shared svn state.
+Add hours/minutes/seconds in the waterfall's ETA display.
+Trial: count Doctest lines too.
+ShellCommand: record more info in the headers: stdin closing, PTY usage.
+Make it possible to stop builds across reconfig boundaries.
+SVN revision numbers are now passed as strings, which was breaking MailNotifier
+
+** Deprecation Schedule
+
+The changes.freshcvsmail change source was replaced by
+changes.mail.FCMaildirSource in 0.7.6, and has been removed in 0.7.7 .
+
+c['sources'] (plural) was replaced by c['change_source'] (singular) in 0.7.6,
+and will be removed by 0.8.0.
+
+c['bots'] was replaced by c['buildslaves'] in 0.7.6, and will be removed by
+0.8.0 . c['bots'] only accepts BuildSlave instances, not name/passwd tuples.
+
+The html.Waterfall status target was replaced by html.WebStatus in 0.7.6, and
+will be removed by 0.8.0.
+
+
+* Release 0.7.6 (30 Sep 2007)
+
+** Things You Need To Know
+
+*** 'buildbot upgrade-master'
+
+Each time you install a new version of Buildbot, you should run the new
+'buildbot upgrade-master' command on each of your pre-existing buildmasters.
+This will add files and fix (or at least detect) incompatibilities between
+your old config and the new code.
+
+*** new WebStatus page
+
+The Waterfall has been replaced by the more general WebStatus display,
+described below. WebStatus serves static files from a new public_html/
+directory that lives in the buildmaster's basedir. Files like index.html,
+buildbot.css, and robots.txt are served directly from that directory, so any
+modifications you wish to make should be made to those files. In particular,
+any custom CSS you've written should be copied into public_html/buildbot.css.
+The 'upgrade-master' command will populate this directory for you.
+
+The old Waterfall page is deprecated, but it should continue to work for
+another few releases. It is now a subclass of WebStatus which just replaces
+the default root URL with another copy of the /waterfall resource.
+
+*** Compatibility: Python-2.3 or newer, Twisted-2.0 or newer
+
+No compatiblity losses here, buildbot-0.7.6 is compatible with the same
+versions of python and twisted that 0.7.5 was.
+
+Buildbot is tested on a regular basis (http://buildbot.buildbot.net) against
+nearly a full matrix of Python-(2.3,2.4,2.5) * Twisted-(2.0,2.1,2.2,2.4,2.5).
+
+*** New Buildbot Home Page
+
+Buildbot has moved to a new Trac instance at http://buildbot.net/ , and all
+new bugs and tickets should be filed there. The old sourceforge bugs at
+http://buildbot.sf.net/ will slowly be migrated over. Mailing lists are still
+managed at sourceforge, and downloads are still available there.
+
+*** Changed/Deprecated master.cfg Keys and Classes
+
+c['sources'] (plural) has been replaced by c['change_source'] (singular).
+
+c['bots'] has been replaced by c['buildslaves'], and it expects a list of
+BuildSlave instances instead of tuples. See below for more details.
+
+The 'freshcvsmail' change source has been deprecated, and will be removed in
+the next release.
+
+The html.Waterfall status target has been deprecated, and replaced by
+html.WebStatus .
+
+** New Features
+
+*** WebStatus
+
+The new WebStatus display is a superset of the old Waterfall. It contains a
+waterfall as a sub-page, but it also contains pages with more compact
+representations of recent build status. The "one_line_per_build" page
+contains just that, and "one_box_per_builder" shows just the information from
+the top of the waterfall page (last-finished-build and current-activity).
+
+The initial page (when you hit the root of the web site) is served from
+index.html, and provides links to the Waterfall as well as the other pages.
+
+Most of these pages can be filtered by adding query arguments to the URL.
+Adding "?builder=XYZ" will cause the page to only show results for the given
+builder. Adding "?builder=XYZ&builder=ABC" will show results for either
+builder. "?branch=trunk" will limit the results to builds that involved code
+from the trunk.
+
+The /waterfall page has arguments to hide those annoying "buildslave
+connected" messages, to start and and at arbitrary times, and to auto-refresh
+at a chosen interval (with a hardcoded minimum of 15 seconds). It also has a
+"help" page with forms that will help you add all of these nifty filtering
+arguments.
+
+The recommended practice is to modify the index.html file to include links to
+the filtered pages that you find most useful.
+
+Note that WebStatus defaults to allowForce=False, meaning that the display
+will not offer or accept "Force Build" or "Stop Build" controls. (The old
+Waterfall defaults to allowForce=True).
+
+The new WebStatus pages try very hard to use only relative links, making life
+better when the Buildbot sits behind an HTTP reverse proxy.
+
+In addition, there is a rudimentary XMLRPC server run by the WebStatus
+object. It only has two methods so far, but it will acquire more in the
+future. The first customer of this is a project to add a buildbot plugin to
+Trac.
+
+*** BuildFactory.addStep(Step(args))
+
+BuildFactories can be set up either with a complete list of steps, or by
+calling the .addStep() method repeatedly. The preferred way to provide a step
+is by instantiating it, rather than giving a class/kwargs pair. This gives
+the BuildStep class a chance to examine the arguments (and complain about
+anything it doesn't like) while the config file is being read and problems
+are being logged. For example, the old-style:
+
+ from buildbot.process.factory import BuildFactory, s
+ steps = [s(CVS, cvsroot="blah", mode="copy"),
+          s(Compile, command=["make", "all"]),
+          s(Test, command=["make", "test"]),
+         ]
+ f = BuildFactory(steps)
+
+is now:
+
+ f = BuildFactory()
+ f.addStep( CVS(cvsroot="blah", mode="copy") )
+ f.addStep( Compile(command=["make", "all"]) )
+ f.addStep( Test(command=["make", "test"]) )
+
+Authors of BuildStep subclasses which override __init__ to add new arguments
+must register them with self.addFactoryArguments(**newargs) to make sure that
+those classes will work with this new style, otherwise the new arguments will
+be lost.
+
+Using class/kwargs pairs is deprecated, and will be removed in a future
+release.
+
+
+*** BuildSlave instances, max_builds=, notify_on_missing=
+
+Buildslave specification has changed a lot in this release. The old config:
+
+ c['bots'] = [ ("bot1name", "bot1passwd"),
+               ("bot2name", "bot2passwd") ]
+
+is now:
+
+ from buildbot.buildslave import BuildSlave
+ c['slaves'] = [ BuildSlave("bot1name", "bot1passwd"),
+                 BuildSlave("bot2name", "bot2passwd") ]
+
+This new form gives us the ability to add new controls. The first is
+"max_builds=", which imposes a concurrency limit that is like the usual
+SlaveLock, but gives the buildmaster the opportunity to find a different
+slave to run the build. (the buildslave is chosen before the SlaveLock is
+claimed, so pure SlaveLocks don't let you take full advantage of build
+farms).
+
+The other addition is "notify_on_missing=", which accepts an email address
+(or list of addresses), and sends a message when the buildslave has been
+disconnected for more than an hour (configurable with missing_timeout=). This
+may be useful when you expect that the buildslave hosts should be available
+most of the time, and want to investigate the reasons that it went offline.
+
+
+** Other Improvements
+
+The IRC bot has been refactored to make it easier to add instant-messaging
+status delivery in the future. The IM plugins are not yet written, though.
+
+When multiple buildslaves are available for a given build, one of them will
+be picked at random. In previous releases, the first one on the list was
+always picked. This helps to add a certain measure of load-balancing. More
+improvements will be made in the future.
+
+When the buildslave does a VC checkout step that requires clobbering the
+build directory (i.e. in all modes except for 'update'), the buildslave will
+first set the permissions on all build files to allow their deletion, before
+it attempts to delete them. This should fix some problems in which a build
+process left non-user-writable files lying around (frequently a result of
+enthusiastic unit tests).
+
+The BuildStep's workdir= argument can now accept a WithProperties()
+specification, allowing greater control over the workdir.
+
+Support for the 'Bazaar' version control system (/usr/bin/bzr) has been
+added, using the buildbot.steps.source.Bzr class. This is a replacement for
+the old 'Arch' (/usr/bin/tla and /usr/bin/baz) systems, which are still
+supported by Buildbot with the source.Arch and source.Bazaar classes,
+respectively. Unfortunately the old baz system claimed the 'Bazaar' classname
+early, so the new system must use source.Bzr instead of the desired
+source.Bazaar . A future release might change this.
+
+A rudimentary Gnome Panel applet is provided in contrib/bb_applet.py, which
+provides 'buildbot statusgui' -like colored status boxes inside the panel.
+Installing it is a bit tricky, though.
+
+The 'buildbot try' command now accepts a '--diff=foo.patch' argument, to let
+you provide a pre-computed patch. This makes it easier to test out patches
+that you've looked over for safety, without first applying them to your local
+source tree.
+
+A new Mercurial change source was added, hg_buildbot.py, which runs as an
+in-process post-commit hook. This gives us access to much more information
+about the change, as well as being much faster.
+
+The email-based changesource have been refactored, to make it easier to write
+new mail parsers. A parser for the SVN "commit-email.pl" script has been
+added.
+
+** Bugs Fixed
+
+Far too many to count. Please see
+http://buildbot.net/trac/query?status=closed&milestone=0.7.6 for a partial
+list of tickets closed for this release, and the ChangeLog for a complete
+list of all changes since 0.7.5 .
+
+
+* Release 0.7.5 (10 Dec 2006)
+
+** Things You Need To Know
+
+*** The Great BuildStep Renaming
+
+All BuildSteps have moved! They used to be classes in buildbot.process.step,
+but now they all have separate modules in buildbot.steps.* . They have been
+split out into separate categories: for example, the source checkout steps
+are now buildbot.steps.source.CVS, buildbot.steps.source.Darcs, etc. The most
+commonly used one is probably buildbot.steps.shell.ShellCommand . The
+python-specific steps are in buildbot.steps.python, and the Twisted-specific
+steps are in buildbot.steps.python_twisted .
+
+You will need to update your master.cfg files to use the new names. The old
+names are deprecated and will be removed altogether in the next release.
+
+*** Compatibility
+
+Buildbot now requires python-2.3 or later. Buildbot now requires
+Twisted-2.0.0 or later. Support for earlier versions of both has finally been
+removed. If you discover it works with unsupported versions, please return
+your Buildbot to the factory for repairs :-).
+
+Buildbot has *not* yet been tested against the recent python-2.5 release. It
+has been tested against the latest SVN version of Twisted, but only in
+conjunction with python-2.4 .
+
+** new features
+
+*** reconfiguring a Builder no longer causes a disconnect/reconnect cycle
+
+This means that sending SIGHUP to the master or running 'buildbot reconfig
+MASTERDIR' command no longer interrupts any current builds, nor does it lose
+pending builds like it did before. This involved a fairly substantial
+refactoring of the various internal BotPerspective/BotMaster/Builder classes.
+Note that reconfiguring Schedulers still loses any Changes that were waiting
+for the tree to become stable: hopefully this will be fixed in the next
+release.
+
+*** 'buildbot start/restart/reconfig' now show logs until startup is complete
+
+These commands now have additional code to follow twistd.log and display all
+the lines that are emitted from the beginning of the start/reconfig action
+until it has completed. This gives you a chance to see any problems detected
+in the config file without needing to manually look in twistd.log or use
+another shell to 'tail -f' it. This also makes it clear which config file is
+being used. This functionality is not available under windows.
+
+In addition, if any problems are detected during 'start' or 'restart' (but
+not reconfig), the buildbot command will terminate with a non-zero exit
+status, making it easier to use in scripts. Closes SF#1517975.
+
+*** Locks now take maxCount=N to allow multiple simultaneous owners
+
+This allows Locks to be non-exclusive but still limit maximum concurrency.
+Thanks to James Knight for the patch. Closes SF#1434997.
+
+*** filetransfer steps
+
+buildbot.steps.transfer.FileUpload is a buildstep that will move files from
+the slave to the master. Likewise, FileDownload will move files from the
+master down to the buildslave. Many thanks to Albert Hofkamp for contributing
+these classes. Closes SF#1504631.
+
+*** pyflakes step
+
+buildbot.steps.python.PyFlakes will run the simple 'pyflakes' static analysis
+tool and parse the results to tell you about undefined names, unused imports,
+etc. You'll need to tell it how to run pyflakes, usually with something like
+command=["pyflakes", "src/packagedir"] or the like. The default command is
+"make pyflakes", which assumes that you have a suitable target in your
+top-level Makefile.
+
+*** Monotone support
+
+Nathaniel Smith has contributed initial support for the Monotone version
+control system. The code still needs docs and tests, but on the other hand it
+has been in use by the Monotone buildbot for a long time now, so it is
+probably fairly stable.
+
+*** Tinderbox support
+
+Ben Hearsum and the Mozilla crew have contributed some classes to allow
+Buildbot to work with Tinderbox clients. One piece is
+buildbot.changes.bonsaipoller.BonsaiPoller, which is a ChangeSource that
+polls a Bonsai server (which is a kind of web-vased viewcvs CGI script) to
+discover source code changes. The other piece is
+buildbot.status.tinderbox.TinderboxMailNotifier, which is a status plugin
+that sends email in the same format as Tinderbox does, which allows a number
+of Tinderbox tools to be driven by Buildbot instead.
+
+*** SVN Poller
+
+Niklaus Giger contributed a ChangeSource (buildbot.changes.svnpoller) which
+polls a remote SVN repository on a periodic basis. This is useful when, for
+whatever reason, you cannot add a post-commit hook script to the repository.
+This obsoletes the external contrib/svn_watcher.py script.
+
+** notes for plugin developers
+
+*** IStatusLog.readlines()
+
+This new method makes it easier for a status plugin (or a
+BuildStep.createSummary method) to walk through a StatusLog one line at a
+time. For example, if you wanted to create an extra logfile that just
+contained all the GCC warnings from the main log, you could use the
+following:
+
+    def createSummary(self, log):
+        warnings = []
+        for line in log.readlines():
+            if "warning:" in line:
+                warnings.append()
+        self.addCompleteLog('warnings', "".join(warnings))
+
+The "BuildStep LogFiles" section of the user's manual contains more
+information. This method is not particularly memory-efficient yet (it reads
+the whole logfile into memory first, then splits it into lines); this will be
+improved in a future release.
+
+** bug fixes
+
+*** Update source.SVN to work with the new SVN-1.4.0
+
+The latest subversion changed the behavior in an unusual situation which
+caused the unit tests to fail. This was unlikely to cause a problem in actual
+usage, but the tests have been updated to pass with the new version.
+
+*** update svn_buildbot.py to avoid mangling filenames
+
+Older versions of this script were stripping the wrong number of columns from
+the output of 'svnlook changed', and would sometimes mangle filenames. This
+has been fixed. Closes SF#1545146.
+
+*** logfiles= caused subsequent build failures under Windows
+
+Earlier versions of buildbot didn't explicitly close any logfiles= file
+handles when the build finished. On windows (where you cannot delete a file
+that someone else is reading), this could cause the next build to fail as the
+source checkout step was unable to delete the old working directory. This has
+been fixed. Closes SF#1568415.
+
+*** logfiles= didn't work on OS-X
+
+Macintosh OS-X has a different behavior when reading files that have reached
+EOF, the result was that logfiles= sometimes didn't work. Thanks to Mark Rowe
+for the patch.
+
+** other changes
+
+The 'buildbot sighup MASTERDIR' command has been replaced with 'buildbot
+reconfig MASTERDIR', since that seems to be a slightly more meaningful name.
+The 'sighup' form will remain as an alias.
+
+
+* Release 0.7.4 (23 Aug 2006)
+
+** Things You Need To Know
+
+The PBChangeSource's prefix= argument has changed, you probably need to add a
+slash now. This is mostly used by sites which use Subversion and
+svn_buildbot.py.
+
+The subcommands that are used to create a buildmaster or a buildslave have
+changed. They used to be called 'buildbot master' and 'buildbot slave'. Now
+they are called 'buildbot create-master' and 'buildbot create-slave'. Zipf's
+Law suggests that these are more appropriate names for these
+infrequently-used commands.
+
+The syntax for the c['manhole'] feature has changed.
+
+** new features
+
+*** full Perforce support
+
+SF#1473939: large patch from Scott Lamb, with docs and unit tests! This
+includes both the step.P4 source-checkout BuildStep, and the changes.p4poller
+ChangeSource you'll want to feed it. P4 is now supported just as well as all
+the other VC systems. Thanks Scott!
+
+*** SSH-based Manhole
+
+The 'manhole' feature allows buildbot developers to get access to a python
+read/eval/print loop (REPL) inside the buildmaster through a network
+connection. Previously, this ran over unencrypted telnet, using a simple
+username/password for access control. The new release defaults to encrypted
+SSH access, using either username/password or an authorized_keys file (just
+like sshd). There also exists an unencrypted telnet form, but its use is
+discouraged. The syntax for setting up a manhole has changed, so master.cfg
+files that use them must be updated. The "Debug options" section in the
+user's manual provides a complete description.
+
+*** Multiple Logfiles
+
+BuildSteps can watch multiple log files in realtime, not just stdout/stderr.
+This works in a similar fashion to 'tail -f': the file is polled once per
+second, and any new data is sent to the buildmaster.
+
+This requires a buildslave running 0.7.4 or later, and a warning message is
+produced if used against an old buildslave (which will otherwise produce no
+data). Use "logfiles={'name': 'filename'}" to take advantage of this feature
+from master.cfg, and see the "ShellCommand" section of the user's manual for
+full documentation.
+
+The 'Trial' buildstep has been updated to use this, to display
+_trial_temp/test.log in realtime. It also knows to fall back to the previous
+"cat" command if the buildslave is too old.
+
+*** BuildStep URLs
+
+BuildSteps can now add arbitrary URLs which will be displayed on the
+Waterfall page in the same place that Logs are presented. This is intended to
+provide a link to generated HTML pages, such as the output of a code coverage
+tool. The step is responsible for somehow uploading the HTML to a web server:
+this feature merely provides an easy way to present the HREF link to the
+user. See the "BuildStep URLs" section of the user's manual for details and
+examples.
+
+*** LogObservers
+
+BuildSteps can now attach LogObservers to various logfiles, allowing them to
+get real-time log output. They can use this to watch for progress-indicating
+events (like counting the number of files compiled, or the number of tests
+which have run), and update both ETA/progress-tracking and step text. This
+allows for more accurate ETA information, and more information passed to the
+user about how much of the process has completed.
+
+The 'Trial' buildstep has been updated to use this for progress tracking, by
+counting how many test cases have run.
+
+** new documentation
+
+What classes are useful in your master.cfg file? A table of them has been
+added to the user's manual, in a section called "Index of Useful Classes".
+
+Want a list of all the keys in master.cfg? Look in the "Index of master.cfg
+keys" section.
+
+A number of pretty diagrams have been added to the "System Architecture"
+portion of the manual, explaining how all the buildbot pieces fit together.
+
+An HTML form of the user's manual is now shipped in the source tarball. This
+makes it a bit bigger: sorry about that. The old PyCon-2003 paper has been
+removed from the distribution, as it is mostly supplanted by the user's
+manual by this point.
+
+** bugfixes
+
+SF#1217699 + SF#1381867: The prefix= argument to PBChangeSource has been
+changed: now it does just a simple string-prefix match and strip. The
+previous behavior was buggy and unhelpful. NOTE: if you were using prefix=
+before, you probably need to add a slash to the end of it.
+
+SF#1398174: ignore SVN property changes better, fixed by Olivier Bonnet
+
+SF#1452801: don't double-escape the build URL, fixed by Olivier Bonnet
+
+SF#1401121: add support for running py2exe on windows, by Mark Hammond
+
+reloading unchanged config files with WithProperties shouldn't change anything.
+
+All svn commands now include --non-interactive so they won't ask for
+passwords. Instead, the command will fail if it cannot be performed without
+user input.
+
+Deprecation warnings with newer versions of Twisted have been hushed.
+
+** compatibility
+
+I haven't actually removed support for Twisted-1.3.0 yet, but I'd like to.
+
+The step_twisted default value for --reporter matches modern Twisteds,
+though, and won't work under 1.3.0.
+
+ShellCommand.flunkOnFailure now defaults to True, so any shell command which
+fails counts as a build failure. Set this to False if you don't want this
+behavior.
+
+** minor features
+
+contrib/darcs_buildbot.py contains a new script suitable for use in a darcs
+commit-hook.
+
+Hovering a cursor over the yellow "Build #123" box in the Waterfall display
+will pop up an HTML tooltip to show the reason for the build. Thanks to Zandr
+Milewski for the suggestion.
+
+contrib/CSS/*.css now contains several contributed stylesheets to make the
+Waterfall display a bit less ugly. Thanks to John O'Duinn for gathering them.
+
+ShellCommand and its derivatives can now accept either a string or a list of
+strings in the description= and descriptionDone= arguments. Thanks to Paul
+Winkler for the catch.
+
+
+* Release 0.7.3 (23 May 2006)
+
+** compatibility
+
+This release is compatible with Twisted-1.3.0, but the next one will not be.
+Please upgrade to at least Twisted-2.0.x soon, as the next buildbot release
+will require it.
+
+** new features
+
+*** Mercurial support
+
+Support for Mercurial version control system (http://selenic.com/mercurial)
+has been added. This adds a buildbot.process.step.Mercurial BuildStep. A
+suitable hook script to deliver changes to the buildmaster is still missing.
+
+*** 'buildbot restart' command
+
+The 'buildbot restart BASEDIR' command will perform a 'buildbot stop' and
+'buildbot start', and will attempt to wait for the buildbot process to shut
+down in between. This is useful when you need to upgrade the code on your
+buildmaster or buildslave and want to take it down for a minimum amount of
+time.
+
+*** build properties
+
+Each build now has a set of named "Build Properties", which can be set by
+steps and interpolated into ShellCommands. The 'revision' and 'got_revision'
+properties are the most interesting ones available at this point, and can be
+used e.g. to get the VC revision number into the filename of a generated
+tarball. See the user's manual section entited "Build Properties" for more
+details.
+
+** minor features
+
+*** IRC now takes password= argument
+
+Useful for letting your bot claim a persistent identity.
+
+*** svn_buildbot.py is easier to modify to understand branches
+*** BuildFactory has a new .addStep method
+*** p4poller has new arguments
+*** new contrib scripts: viewcvspoll, svnpoller, svn_watcher
+
+These poll an external VC repository to watch for changes, as opposed to
+adding a hook script to the repository that pushes changes into the
+buildmaster. This means higher latency but may be easier to configure,
+especially if you do not have authority on the repository host.
+
+*** VC build property 'got_revision'
+
+The 'got_revision' property reports what revision a VC step actually
+acquired, which may be useful to know when building from HEAD.
+
+*** improved CSS in Waterfall
+
+The Waterfall display has a few new class= tags, which may make it easier to
+write custom CSS to make it look prettier.
+
+*** robots_txt= argument in Waterfall
+
+You can now pass a filename to the robots_txt= argument, which will be served
+as the "robots.txt" file. This can be used to discourage search engine
+spiders from crawling through the numerous build-status pages.
+
+** bugfixes
+
+*** tests more likely to pass on non-English systems
+
+The unit test suite now sets $LANG='C' to make subcommands emit error
+messages in english instead of whatever native language is in use on the
+host. This improves the chances that the unit tests will pass on such
+systems. This affects certain VC-related subcommands too.
+
+test_vc was assuming that the system time was expressed with a numeric
+timezone, which is not always the case, especially under windows. This
+probably works better now than it did before. This only affects the CVS
+tests.
+
+'buildbot try' (for CVS) now uses UTC instead of the local timezone. The
+'got_revision' property is also expressed in UTC. Both should help deal with
+buggy versions of CVS that don't parse numeric timezones properly.
+
+
+* Release 0.7.2 (17 Feb 2006)
+
+** new features
+
+*** all TCP port numbers in config file now accept a strports string
+
+Sometimes it is useful to restrict certain TCP ports that the buildmaster
+listens on to use specific network interfaces. In particular, if the
+buildmaster and SVN repository live on the same machine, you may want to
+restrict the PBChangeSource to only listen on the loopback interface,
+insuring that no external entities can inject Changes into the buildbot.
+Likewise, if you are using something like Apache's reverse-proxy feature to
+provide access to the buildmaster's HTML status page, you might want to hide
+the real Waterfall port by having it only bind to the loopback interface.
+
+To accomplish this, use a string like "tcp:12345:interface=127.0.0.1" instead
+of a number like 12345. These strings are called "strports specification
+strings", and are documented in twisted's twisted.application.strports module
+(you can probably type 'pydoc twisted.application.strports' to see this
+documentation). Pretty much everywhere the buildbot takes a port number will
+now accept a strports spec, and any bare numbers are translated into TCP port
+numbers (listening on all network interfaces) for compatibility.
+
+*** buildslave --umask control
+
+Twisted's daemonization utility (/usr/bin/twistd) automatically sets the
+umask to 077, which means that all files generated by both the buildmaster
+and the buildslave will only be readable by the account under which the
+respective daemon is running. This makes it unnecessarily difficult to share
+build products (e.g. by symlinking ~/public_html/current_docs/ to a directory
+within the slave's build directory where each build puts the results of a
+"make docs" step).
+
+The 'buildbot slave <PARAMS>' command now accepts a --umask argument, which
+can be used to override the umask set by twistd. If you create the buildslave
+with '--umask=022', then all build products will be world-readable, making it
+easier for other processes (run under other accounts) to access them.
+
+** bug fixes
+
+The 0.7.1 release had a bug whereby reloading the config file could break all
+configured Schedulers, causing them to raise an exception when new changes
+arrived but not actually schedule a new build. This has been fixed.
+
+Fixed a bug which caused the AnyBranchScheduler to explode when branch==None.
+Thanks to Kevin Turner for the catch. I also think I fixed a bug whereby the
+TryScheduler would explode when it was given a Change (which it is supposed
+to simply ignore).
+
+The Waterfall display now does more quoting of names (including Builder
+names, BuildStep names, etc), so it is more likely that these names can
+contain unusual characters like spaces, quotes, and slashes. There may still
+be some problems with these kinds of names, however.. please report any bugs
+to the mailing list.
+
+
+* Release 0.7.1 (26 Nov 2005)
+
+** new features
+
+*** scheduler.Nightly
+
+Dobes Vandermeer contributed a cron-style 'Nightly' scheduler. Unlike the
+more-primitive Periodic class (which only lets you specify the duration
+between build attempts), Nightly lets you schedule builds for specific times
+of day, week, month, or year. The interface is very much like the crontab(5)
+file. See the buildbot.scheduler.Nightly docstring for complete details.
+
+** minor new features
+
+*** step.Trial can work with Trial from Twisted >2.1.0
+
+The 'Trial' step now accepts the trialMode= argument, which should be a list
+of strings to be added to trial's argv array. This defaults to ["-to"], which
+is appropriate for the Trial that ships in Twisted-2.1.0 and earlier, and
+tells Trial to emit non-colorized verbose output. To use this step with
+trials from later versions of Twisted, this should be changed to
+["--reporter=bwverbose"].
+
+In addition, you can now set other Trial command-line parameters through the
+trialArgs= argument. This is a list of strings, and defaults to an empty list.
+
+*** Added a 'resubmit this build' button to the web page
+
+*** Make the VC-checkout step's description more useful
+
+Added the word "[branch]" to the VC step's description (used in the Step's
+box on the Waterfall page, among others) when we're checking out a
+non-default branch. Also add "rNNN" where appropriate to indicate which
+revision is being checked out. Thanks to Brad Hards and Nathaniel Smith for
+the suggestion.
+
+** bugs fixed
+
+Several patches from Dobes Vandermeer: Escape the URLs in email, in case they
+have spaces and such. Fill otherwise-empty <td> elements, as a workaround for
+buggy browsers that might optimize them away. Also use binary mode when
+opening status pickle files, to make windows work better. The
+AnyBranchScheduler now works even when you don't provide a fileIsImportant=
+argument.
+
+Stringify the base revision before stuffing it into a 'try' jobfile, helping
+SVN and Arch implement 'try' builds better. Thanks to Steven Walter for the
+patch.
+
+Fix the compare_attrs list in PBChangeSource, FreshCVSSource, and Waterfall.
+Before this, certain changes to these objects in the master.cfg file were
+ignored, such that you would have to stop and re-start the buildmaster to
+make them take effect.
+
+The config file is now loaded serially, shutting down old (or replaced)
+Status/ChangeSource plugins before starting new ones. This fixes a bug in
+which changing an aspect of, say, the Waterfall display would cause an
+exception as both old and new instances fight over the same TCP port. This
+should also fix a bug whereby new Periodic Schedulers could fire a build
+before the Builders have finished being added.
+
+There was a bug in the way Locks were handled when the config file was
+reloaded: changing one Builder (but not the others) and reloading master.cfg
+would result in multiple instances of the same Lock object, so the Locks
+would fail to prevent simultaneous execution of Builds or Steps. This has
+been fixed.
+
+** other changes
+
+For a long time, certain StatusReceiver methods (like buildStarted and
+stepStarted) have been able to return another StatusReceiver instance
+(usually 'self') to indicate that they wish to subscribe to events within the
+new object. For example, if the buildStarted() method returns 'self', the
+status receiver will also receive events for the new build, like
+stepStarted() and buildETAUpdate(). Returning a 'self' from buildStarted() is
+equivalent to calling build.subscribe(self).
+
+Starting with buildbot-0.7.1, this auto-subscribe convenience will also
+register to automatically unsubscribe the target when the build or step has
+finished, just as if build.unsubscribe(self) had been called. Also, the
+unsubscribe() method has been changed to not explode if the same receiver is
+unsubscribed multiple times. (note that it will still explode is the same
+receiver is *subscribed* multiple times, so please continue to refrain from
+doing that).
+
+
+* Release 0.7.0 (24 Oct 2005)
+
+** new features
+
+*** new c['schedulers'] config-file element (REQUIRED)
+
+The code which decides exactly *when* a build is performed has been massively
+refactored, enabling much more flexible build scheduling. YOU MUST UPDATE
+your master.cfg files to match: in general this will merely require you to
+add an appropriate c['schedulers'] entry. Any old ".treeStableTime" settings
+on the BuildFactory instances will now be ignored. The user's manual has
+complete details with examples of how the new Scheduler classes work.
+
+*** c['interlocks'] removed, Locks and Dependencies now separate items
+
+The c['interlocks'] config element has been removed, and its functionality
+replaced with two separate objects. Locks are used to tell the buildmaster
+that certain Steps or Builds should not run at the same time as other Steps
+or Builds (useful for test suites that require exclusive access to some
+external resource: of course the real fix is to fix the tests, because
+otherwise your developers will be suffering from the same limitations). The
+Lock object is created in the config file and then referenced by a Step
+specification tuple or by the 'locks' key of the Builder specification
+dictionary. Locks come in two flavors: MasterLocks are buildmaster-wide,
+while SlaveLocks are specific to a single buildslave.
+
+When you want to have one Build run or not run depending upon whether some
+other set of Builds have passed or failed, you use a special kind of
+Scheduler defined in the scheduler.Dependent class. This scheduler watches an
+upstream Scheduler for builds of a given source version to complete, and only
+fires off its own Builders when all of the upstream's Builders have built
+that version successfully.
+
+Both features are fully documented in the user's manual.
+
+*** 'buildbot try'
+
+The 'try' feature has finally been added. There is some configuration
+involved, both in the buildmaster config and on the developer's side, but
+once in place this allows the developer to type 'buildbot try' in their
+locally-modified tree and to be given a report of what would happen if their
+changes were to be committed. This works by computing a (base revision,
+patch) tuple that describes the developer's tree, sending that to the
+buildmaster, then running a build with that source on a given set of
+Builders. The 'buildbot try' tool then emits status messages until the builds
+have finished.
+
+'try' exists to allow developers to run cross-platform tests on their code
+before committing it, reducing the chances they will inconvenience other
+developers by breaking the build. The UI is still clunky, but expect it to
+change and improve over the next few releases.
+
+Instructions for developers who want to use 'try' (and the configuration
+changes necessary to enable its use) are in the user's manual.
+
+*** Build-On-Branch
+
+When suitably configured, the buildbot can be used to build trees from a
+variety of related branches. You can set up Schedulers to build a tree using
+whichever branch was last changed, or users can request builds of specific
+branches through IRC, the web page, or (eventually) the CLI 'buildbot force'
+subcommand.
+
+The IRC 'force' command now takes --branch and --revision arguments (not that
+they always make sense). Likewise the HTML 'force build' button now has an
+input field for branch and revision. Your build's source-checkout step must
+be suitably configured to support this: for SVN it involves giving both a
+base URL and a default branch. Other VC systems are configured differently.
+The ChangeSource must also provide branch information: the 'buildbot
+sendchange' command now takes a --branch argument to help hook script writers
+accomplish this.
+
+*** Multiple slaves per Builder
+
+You can now attach multiple buildslaves to each Builder. This can provide
+redundancy or primitive load-balancing among many machines equally capable of
+running the build. To use this, define a key in the Builder specification
+dictionary named 'slavenames' with a list of buildslave names (instead of the
+usual 'slavename' that contains just a single slavename).
+
+*** minor new features
+
+The IRC and email status-reporting facilities now provide more specific URLs
+for particular builds, in addition to the generic buildmaster home page. The
+HTML per-build page now has more information.
+
+The Twisted-specific test classes have been modified to match the argument
+syntax preferred by Trial as of Twisted-2.1.0 and newer. The generic trial
+steps are still suitable for the Trial that comes with older versions of
+Twisted, but may produce deprecation warnings or errors when used with the
+latest Trial.
+
+** bugs fixed
+
+DNotify, used by the maildir-watching ChangeSources, had problems on some
+64-bit systems relating to signed-vs-unsigned constants and the DN_MULTISHOT
+flag. A workaround was provided by Brad Hards.
+
+The web status page should now be valid XHTML, thanks to a patch by Brad
+Hards. The charset parameter is specified to be UTF-8, so VC comments,
+builder names, etc, should probably all be in UTF-8 to be displayed properly.
+
+** creeping version dependencies
+
+The IRC 'force build' command now requires python2.3 (for the shlex.split
+function).
+
+
+* Release 0.6.6 (23 May 2005)
+
+** bugs fixed
+
+The 'sendchange', 'stop', and 'sighup' subcommands were broken, simple bugs
+that were not caught by the test suite. Sorry.
+
+The 'buildbot master' command now uses "raw" strings to create .tac files
+that will still function under windows (since we must put directory names
+that contain backslashes into that file).
+
+The keep-on-disk behavior added in 0.6.5 included the ability to upgrade old
+in-pickle LogFile instances. This upgrade function was not added to the
+HTMLLogFile class, so an exception would be raised when attempting to load or
+display any build with one of these logs (which are normally used only for
+showing build exceptions). This has been fixed.
+
+Several unnecessary imports were removed, so the Buildbot should function
+normally with just Twisted-2.0.0's "Core" module installed. (of course you
+will need TwistedWeb, TwistedWords, and/or TwistedMail if you use status
+targets that require them). The test suite should skip all tests that cannot
+be run because of missing Twisted modules.
+
+The master/slave's basedir is now prepended to sys.path before starting the
+daemon. This used to happen implicitly (as a result of twistd's setup
+preamble), but 0.6.5 internalized the invocation of twistd and did not copy
+this behavior. This change restores the ability to access "private.py"-style
+modules in the basedir from the master.cfg file with a simple "import
+private" statement. Thanks to Thomas Vander Stichele for the catch.
+
+
+* Release 0.6.5 (18 May 2005)
+
+** deprecated config keys removed
+
+The 'webPortnum', 'webPathname', 'irc', and 'manholePort' config-file keys,
+which were deprecated in the previous release, have now been removed. In
+addition, Builders must now always be configured with dictionaries: the
+support for configuring them with tuples has been removed.
+
+** master/slave creation and startup changed
+
+The buildbot no longer uses .tap files to store serialized representations of
+the buildmaster/buildslave applications. Instead, this release now uses .tac
+files, which are human-readable scripts that create new instances (rather
+than .tap files, which were pickles of pre-created instances). 'mktap
+buildbot' is gone.
+
+You will need to update your buildbot directories to handle this. The
+procedure is the same as creating a new buildmaster or buildslave: use
+'buildbot master BASEDIR' or 'buildbot slave BASEDIR ARGS..'. This will
+create a 'buildbot.tac' file in the target directory. The 'buildbot start
+BASEDIR' will use twistd to start the application.
+
+The 'buildbot start' command now looks for a Makefile.buildbot, and if it
+finds one (and /usr/bin/make exists), it will use it to start the application
+instead of calling twistd directly. This allows you to customize startup,
+perhaps by adding environment variables. The setup commands create a sample
+file in Makefile.sample, but you must copy this to Makefile.buildbot to
+actually use it. The previous release looked for a bare 'Makefile', and also
+installed a 'Makefile', so you were always using the customized approach,
+even if you didn't ask for it. That old Makefile launched the .tap file, so
+changing names was also necessary to make sure that the new 'buildbot start'
+doesn't try to run the old .tap file.
+
+'buildbot stop' now uses os.kill instead of spawning an external process,
+making it more likely to work under windows. It waits up to 5 seconds for the
+daemon to go away, so you can now do 'buildbot stop BASEDIR; buildbot start
+BASEDIR' with less risk of launching the new daemon before the old one has
+fully shut down. Likewise, 'buildbot start' imports twistd's internals
+directly instead of spawning an external copy, so it should work better under
+windows.
+
+** new documentation
+
+All of the old Lore-based documents were converted into a new Texinfo-format
+manual, and considerable new text was added to describe the installation
+process. The docs are not yet complete, but they're slowly shaping up to form
+a proper user's manual.
+
+** new features
+
+Arch checkouts can now use precise revision stamps instead of always using
+the latest revision. A separate Source step for using Bazaar (an alternative
+Arch client) instead of 'tla' was added. A Source step for Cogito (the new
+linux kernel VC system) was contributed by Brandon Philips. All Source steps
+now accept a retry= argument to indicate that failing VC checkouts should be
+retried a few times (SF#1200395), note that this requires an updated
+buildslave.
+
+The 'buildbot sendchange' command was added, to be used in VC hook scripts to
+send changes at a pb.PBChangeSource . contrib/arch_buildbot.py was added to
+use this tool; it should be installed using the 'Arch meta hook' scheme.
+
+Changes can now accept a branch= parameter, and Builders have an
+isBranchImportant() test that acts like isFileImportant(). Thanks to Thomas
+Vander Stichele. Note: I renamed his tag= to branch=, in anticipation of an
+upcoming feature to build specific branches. "tag" seemed too CVS-centric.
+
+LogFiles have been rewritten to stream the incoming data directly to disk
+rather than keeping a copy in memory all the time (SF#1200392). This
+drastically reduces the buildmaster's memory requirements and makes 100MB+
+log files feasible. The log files are stored next to the serialized Builds,
+in files like BASEDIR/builder-dir/12-log-compile-output, so you'll want a
+cron job to delete old ones just like you do with old Builds. Old-style
+Builds from 0.6.4 and earlier are converted when they are first read, so the
+first load of the Waterfall display after updating to this release may take
+quite some time.
+
+** build process updates
+
+BuildSteps can now return a status of EXCEPTION, which terminates the build
+right away. This allows exceptions to be caught right away, but still make
+sure the build stops quickly.
+
+** bug fixes
+
+Some more windows incompatibilities were fixed. The test suite now has two
+failing tests remaining, both of which appear to be Twisted issues that
+should not affect normal operation.
+
+The test suite no longer raises any deprecation warnings when run against
+twisted-2.0 (except for the ones which come from Twisted itself).
+
+
+* Release 0.6.4 (28 Apr 2005)
+
+** major bugs fixed
+
+The 'buildbot' tool in 0.6.3, when used to create a new buildmaster, failed
+unless it found a 'changes.pck' file. As this file is created by a running
+buildmaster, this made 0.6.3 completely unusable for first-time
+installations. This has been fixed.
+
+** minor bugs fixed
+
+The IRC bot had a bug wherein asking it to watch a certain builder (the "I'll
+give a shout when the build finishes" message) would cause an exception, so
+it would not, in fact, shout. The HTML page had an exception in the "change
+sources" page (reached by following the "Changes" link at the top of the
+column that shows the names of commiters). Re-loading the config file while
+builders were already attached would result in a benign error message. The
+server side of the PBListener status client had an exception when providing
+information about a non-existent Build (e.g., when the client asks for the
+Build that is currently running, and the server says "None").
+
+These bugs have all been fixed.
+
+The unit tests now pass under python2.2; they were failing before because of
+some 2.3isms that crept in. More unit tests which failed under windows now
+pass, only one (test_webPathname_port) is still failing.
+
+** 'buildbot' tool looks for a .buildbot/options file
+
+The 'statusgui' and the 'debugclient' subcommands can both look for a
+.buildbot/ directory, and an 'options' file therein, to extract default
+values for the location of the buildmaster. This directory is searched in the
+current directory, its parent, etc, all the way up to the filesystem root
+(assuming you own the directories in question). It also look in ~/.buildbot/
+for this file. This feature allows you to put a .buildbot at the top of your
+working tree, telling any 'buildbot' invocations you perform therein how to
+get to the buildmaster associated with that tree's project.
+
+Windows users get something similar, using %APPDATA%/buildbot instead of
+~/.buildbot .
+
+** windows ShellCommands are launched with 'cmd.exe'
+
+The buildslave has been modified to run all list-based ShellCommands by
+prepending [os.environ['COMSPEC'], '/c'] to the argv list before execution.
+This should allow the buildslave's PATH to be searched for commands,
+improving the chances that it can run the same 'trial -o foo' commands as a
+unix buildslave. The potential downside is that spaces in argv elements might
+be re-parsed, or quotes might be re-interpreted. The consensus on the mailing
+list was that this is a useful thing to do, but please report any problems
+you encounter with it.
+
+** minor features
+
+The Waterfall display now shows the buildbot's home timezone at the top of
+the timestamp column. The default favicon.ico is now much nicer-looking (it
+is generated with Blender.. the icon.blend file is available in CVS in
+docs/images/ should you care to play with it).
+
+
+
+* Release 0.6.3 (25 Apr 2005)
+
+** 'buildbot' tool gets more uses
+
+The 'buildbot' executable has acquired three new subcommands. 'buildbot
+debugclient' brings up the small remote-control panel that connects to a
+buildmaster (via the slave port and the c['debugPassword']). This tool,
+formerly in contrib/debugclient.py, lets you reload the config file, force
+builds, and simulate inbound commit messages. It requires gtk2, glade, and
+the python bindings for both to be installed.
+
+'buildbot statusgui' brings up a live status client, formerly available by
+running buildbot/clients/gtkPanes.py as a program. This connects to the PB
+status port that you create with:
+
+  c['status'].append(client.PBListener(portnum))
+
+and shows two boxes per Builder, one for the last build, one for current
+activity. These boxes are updated in realtime. The effect is primitive, but
+is intended as an example of what's possible with the PB status interface.
+
+'buildbot statuslog' provides a text-based running log of buildmaster events.
+
+Note: command names are subject to change. These should get much more useful
+over time.
+
+** web page has a favicon
+
+When constructing the html.Waterfall instance, you can provide the filename
+of an image that will be provided when the "favicon.ico" resource is
+requested. Many web browsers display this as an icon next to the URL or
+bookmark. A goofy little default icon is included.
+
+** web page has CSS
+
+Thanks to Thomas Vander Stichele, the Waterfall page is now themable through
+CSS. The default CSS is located in buildbot/status/classic.css, and creates a
+page that is mostly identical to the old, non-CSS based table.
+
+You can specify a different CSS file to use by passing it as the css=
+argument to html.Waterfall(). See the docstring for Waterfall for some more
+details.
+
+** builder "categories"
+
+Thomas has added code which places each Builder in an optional "category".
+The various status targets (Waterfall, IRC, MailNotifier) can accept a list
+of categories, and they will ignore any activity in builders outside this
+list. This makes it easy to create some Builders which are "experimental" or
+otherwise not yet ready for the world to see, or indicate that certain
+builders should not harass developers when their tests fail, perhaps because
+the build slaves for them are not yet fully functional.
+
+** Deprecated features
+
+*** defining Builders with tuples is deprecated
+
+For a long time, the preferred way to define builders in the config file has
+been with a dictionary. The less-flexible old style of a 4-item tuple (name,
+slavename, builddir, factory) is now officially deprecated (i.e., it will
+emit a warning if you use it), and will be removed in the next release.
+Dictionaries are more flexible: additional keys like periodicBuildTime are
+simply unavailable to tuple-defined builders.
+
+Note: it is a good idea to watch the logfile (usually in twistd.log) when you
+first start the buildmaster, or whenever you reload the config file. Any
+warnings or errors in the config file will be found there.
+
+*** c['webPortnum'], c['webPathname'], c['irc'] are deprecated
+
+All status reporters should be defined in the c['status'] array, using
+buildbot.status.html.Waterfall or buildbot.status.words.IRC . These have been
+deprecated for a while, but this is fair warning that these keys will be
+removed in the next release.
+
+*** c['manholePort'] is deprecated
+
+Again, this has been deprecated for a while, in favor of:
+
+ c['manhole'] = master.Manhole(port, username, password)
+
+The preferred syntax will eventually let us use other, better kinds of debug
+shells, such as the experimental curses-based ones in the Twisted sandbox
+(which would offer command-line editing and history).
+
+** bug fixes
+
+The waterfall page has been improved a bit. A circular-reference bug in the
+web page's TextLog class was fixed, which caused a major memory leak in a
+long-running buildmaster with large logfiles that are viewed frequently.
+Modifying the config file in a way which only changed a builder's base
+directory now works correctly. The 'buildbot' command tries to create
+slightly more useful master/slave directories, adding a Makefile entry to
+re-create the .tap file, and removing global-read permissions from the files
+that may contain buildslave passwords.
+
+** twisted-2.0.0 compatibility
+
+Both buildmaster and buildslave should run properly under Twisted-2.0 . There
+are still some warnings about deprecated functions, some of which could be
+fixed, but there are others that would require removing compatibility with
+Twisted-1.3, and I don't expect to do that until 2.0 has been out and stable
+for at least several months. The unit tests should pass under 2.0, whereas
+the previous buildbot release had tests which could hang when run against the
+new "trial" framework in 2.0.
+
+The Twisted-specific steps (including Trial) have been updated to match 2.0
+functionality.
+
+** win32 compatibility
+
+Thankt to Nick Trout, more compatibility fixes have been incorporated,
+improving the chances that the unit tests will pass on windows systems. There
+are still some problems, and a step-by-step "running buildslaves on windows"
+document would be greatly appreciated.
+
+** API docs
+
+Thanks to Thomas Vander Stichele, most of the docstrings have been converted
+to epydoc format. There is a utility in docs/gen-reference to turn these into
+a tree of cross-referenced HTML pages. Eventually these docs will be
+auto-generated and somehow published on the buildbot web page.
+
+
+
+* Release 0.6.2 (13 Dec 2004)
+
+** new features
+
+It is now possible to interrupt a running build. Both the web page and the
+IRC bot feature 'stop build' commands, which can be used to interrupt the
+current BuildStep and accelerate the termination of the overall Build. The
+status reporting for these still leaves something to be desired (an
+'interrupt' event is pushed into the column, and the reason for the interrupt
+is added to a pseudo-logfile for the step that was stopped, but if you only
+look at the top-level status it appears that the build failed on its own).
+
+Builds are also halted if the connection to the buildslave is lost. On the
+slave side, any active commands are halted if the connection to the
+buildmaster is lost.
+
+** minor new features
+
+The IRC log bot now reports ETA times in a MMSS format like "2m45s" instead
+of the clunky "165 seconds".
+
+** bug fixes
+
+*** Slave Disconnect
+
+Slave disconnects should be handled better now: the current build should be
+abandoned properly. Earlier versions could get into weird states where the
+build failed to finish, clogging the builder forever (or at least until the
+buildmaster was restarted).
+
+In addition, there are weird network conditions which could cause a
+buildslave to attempt to connect twice to the same buildmaster. This can
+happen when the slave is sending large logfiles over a slow link, while using
+short keepalive timeouts. The buildmaster has been fixed to allow the second
+connection attempt to take precedence over the first, so that the older
+connection is jettisoned to make way for the newer one.
+
+In addition, the buildslave has been fixed to be less twitchy about timeouts.
+There are now two parameters: keepaliveInterval (which is controlled by the
+mktap 'keepalive' argument), and keepaliveTimeout (which requires editing the
+.py source to change from the default of 30 seconds). The slave expects to
+see *something* from the master at least once every keepaliveInterval
+seconds, and will try to provoke a response (by sending a keepalive request)
+'keepaliveTimeout' seconds before the end of this interval just in case there
+was no regular traffic. Any kind of traffic will qualify, including
+acknowledgements of normal build-status updates.
+
+The net result is that, as long as any given PB message can be sent over the
+wire in less than 'keepaliveTimeout' seconds, the slave should not mistakenly
+disconnect because of a timeout. There will be traffic on the wire at least
+every 'keepaliveInterval' seconds, which is what you want to pay attention to
+if you're trying to keep an intervening NAT box from dropping what it thinks
+is an abandoned connection. A quiet loss of connection will be detected
+within 'keepaliveInterval' seconds.
+
+*** Large Logfiles
+
+The web page rendering code has been fixed to deliver large logfiles in
+pieces, using a producer/consumer apparatus. This avoids the large spike in
+memory consumption when the log file body was linearized into a single string
+and then buffered in the socket's application-side transmit buffer. This
+should also avoid the 640k single-string limit for web.distrib servers that
+could be hit by large (>640k) logfiles.
+
+
+
+* Release 0.6.1 (23 Nov 2004)
+
+** win32 improvements/bugfixes
+
+Several changes have gone in to improve portability to non-unix systems. It
+should be possible to run a build slave under windows without major issues
+(although step-by-step documentation is still greatly desired: check the
+mailing list for suggestions from current win32 users).
+
+*** PBChangeSource: use configurable directory separator, not os.sep
+
+The PBChangeSource, which listens on a TCP socket for change notices
+delivered from tools like contrib/svn_buildbot.py, was splitting source
+filenames with os.sep . This is inappropriate, because those file names are
+coming from the VC repository, not the local filesystem, and the repository
+host may be running a different OS (with a different separator convention)
+than the buildmaster host. In particular, a win32 buildmaster using a CVS
+repository running on a unix box would be confused.
+
+PBChangeSource now takes a sep= argument to indicate the separator character
+to use.
+
+*** build saving should work better
+
+windows cannot do the atomic os.rename() trick that unix can, so under win32
+the buildmaster falls back to save/delete-old/rename, which carries a slight
+risk of losing a saved build log (if the system were to crash between the
+delete-old and the rename).
+
+** new features
+
+*** test-result tracking
+
+Work has begun on fine-grained test-result handling. The eventual goal is to
+be able to track individual tests over time, and create problem reports when
+a test starts failing (which then are resolved when the test starts passing
+again). The first step towards this is an ITestResult interface, and code in
+the TrialTestParser to create such results for all non-passing tests (the
+ones for which Trial emits exception tracebacks).
+
+These test results are currently displayed in a tree-like display in a page
+accessible from each Build's page (follow the numbered link in the yellow
+box at the start of each build to get there).
+
+This interface is still in flux, as it really wants to be able to accomodate
+things like compiler warnings and tests that are skipped because of missing
+libraries or unsupported architectures.
+
+** bug fixes
+
+*** VC updates should survive temporary failures
+
+Some VC systems (CVS and SVN in particular) get upset when files are turned
+into directories or vice versa, or when repository items are moved without
+the knowledge of the VC system. The usual symptom is that a 'cvs update'
+fails where a fresh checkout succeeds.
+
+To avoid having to manually intervene, the build slaves' VC commands have
+been refactored to respond to update failures by deleting the tree and
+attempting a full checkout. This may cause some unnecessary effort when,
+e.g., the CVS server falls off the net, but in the normal case it will only
+come into play when one of these can't-cope situations arises.
+
+*** forget about an existing build when the slave detaches
+
+If the slave was lost during a build, the master did not clear the
+.currentBuild reference, making that builder unavailable for later builds.
+This has been fixed, so that losing a slave should be handled better. This
+area still needs some work, I think it's still possible to get both the
+slave and the master wedged by breaking the connection at just the right
+time. Eventually I want to be able to resume interrupted builds (especially
+when the interruption is the result of a network failure and not because the
+slave or the master actually died).
+
+*** large logfiles now consume less memory
+
+Build logs are stored as lists of (type,text) chunks, so that
+stdout/stderr/headers can be displayed differently (if they were
+distinguishable when they were generated: stdout and stderr are merged when
+usePTY=1). For multi-megabyte logfiles, a large list with many short strings
+could incur a large overhead. The new behavior is to merge same-type string
+chunks together as they are received, aiming for a chunk size of about 10kb,
+which should bring the overhead down to a more reasonable level.
+
+There remains an issue with actually delivering large logfiles over, say,
+the HTML interface. The string chunks must be merged together into a single
+string before delivery, which causes a spike in the memory usage when the
+logfile is viewed. This can also break twisted.web.distrib -type servers,
+where the underlying PB protocol imposes a 640k limit on the size of
+strings. This will be fixed (with a proper Producer/Consumer scheme) in the
+next release.
+
+
+* Release 0.6.0 (30 Sep 2004)
+
+** new features
+
+*** /usr/bin/buildbot control tool
+
+There is now an executable named 'buildbot'. For now, this just provides a
+convenient front-end to mktap/twistd/kill, but eventually it will provide
+access to other client functionality (like the 'try' builds, and a status
+client). Assuming you put your buildbots in /var/lib/buildbot/master/FOO,
+you can do 'buildbot create-master /var/lib/buildbot/master/FOO' and it will
+create the .tap file and set up a sample master.cfg for you. Later,
+'buildbot start /var/lib/buildbot/master/FOO' will start the daemon.
+
+
+*** build status now saved in external files, -shutdown.tap unnecessary
+
+The status rewrite included a change to save all build status in a set of
+external files. These files, one per build, are put in a subdirectory of the
+master's basedir (named according to the 'builddir' parameter of the Builder
+configuration dictionary). This helps keep the buildmaster's memory
+consumption small: the (potentially large) build logs are kept on disk
+instead of in RAM. There is a small cache (2 builds per builder) kept in
+memory, but everything else lives on disk.
+
+The big change is that the buildmaster now keeps *all* status in these
+files. It is no longer necessary to preserve the buildbot-shutdown.tap file
+to run a persistent buildmaster. The buildmaster may be launched with
+'twistd -f buildbot.tap' each time, in fact the '-n' option can be added to
+prevent twistd from automatically creating the -shutdown.tap file.
+
+There is still one lingering bug with this change: the Expectations object
+for each builder (which records how long the various steps took, to provide
+an ETA value for the next time) is not yet saved. The result is that the
+first build after a restart will not provide an ETA value.
+
+0.6.0 keeps status in a single file per build, as opposed to 0.5.0 which
+kept status in many subdirectories (one layer for builds, another for steps,
+and a third for logs). 0.6.0 will detect and delete these subdirectories as
+it overwrites them.
+
+The saved builds are optional. To prevent disk usage from growing without
+bounds, you may want to set up a cron job to run 'find' and delete any which
+are too old. The status displays will happily survive without those saved
+build objects.
+
+The set of recorded Changes is kept in a similar file named 'changes.pck'.
+
+
+*** source checkout now uses timestamp/revision
+
+Source checkouts are now performed with an appropriate -D TIMESTAMP (for
+CVS) or -r REVISION (for SVN) marker to obtain the exact sources that were
+specified by the most recent Change going into the current Build. This
+avoids a race condition in which a change might be committed after the build
+has started but before the source checkout has completed, resulting in a
+mismatched set of source files. Such changes are now ignored.
+
+This works by keeping track of repository-wide revision/transaction numbers
+(for version control systems that offer them, like SVN). The checkout or
+update is performed with the highest such revision number. For CVS (which
+does not have them), the timestamp of each commit message is used, and a -D
+argument is created to place the checkout squarely in the middle of the "tree
+stable timer"'s window.
+
+This also provides the infrastructure for the upcoming 'try' feature. All
+source-checkout commands can now obtain a base revision marker and a patch
+from the Build, allowing certain builds to be performed on something other
+than the most recent sources.
+
+See source.xhtml and steps.xhtml for details.
+
+
+*** Darcs and Arch support added
+
+There are now build steps which retrieve a source tree from Darcs and Arch
+repositories. See steps.xhtml for details.
+
+Preliminary P4 support has been added, thanks to code from Dave Peticolas.
+You must manually set up each build slave with an appropriate P4CLIENT: all
+buildbot does is run 'p4 sync' at the appropriate times.
+
+
+*** Status reporting rewritten
+
+Status reporting was completely revamped. The config file now accepts a
+BuildmasterConfig['status'] entry, with a list of objects that perform status
+delivery. The old config file entries which controlled the web status port
+and the IRC bot have been deprecated in favor of adding instances to
+['status']. The following status-delivery classes have been implemented, all
+in the 'buildbot.status' package:
+
+ client.PBListener(port, username, passwd)
+ html.Waterfall(http_port, distrib_port)
+ mail.MailNotifier(fromaddr, mode, extraRecipients..)
+ words.IRC(host, nick, channels)
+
+See the individual docstrings for details about how to use each one. You can
+create new status-delivery objects by following the interfaces found in the
+buildbot.interfaces module.
+
+
+*** BuildFactory configuration process changed
+
+The basic BuildFactory class is now defined in buildbot.process.factory
+rather than buildbot.process.base, so you will have to update your config
+files. factory.BuildFactory is the base class, which accepts a list of Steps
+to run. See docs/factories.xhtml for details.
+
+There are now easier-to-use BuildFactory classes for projects which use GNU
+Autoconf, perl's MakeMaker (CPAN), python's distutils (but no unit tests),
+and Twisted's Trial. Each one takes a separate 'source' Step to obtain the
+source tree, and then fills in the rest of the Steps for you.
+
+
+*** CVS/SVN VC steps unified, simplified
+
+The confusing collection of arguments for the CVS step ('clobber=',
+'copydir=', and 'export=') have been removed in favor of a single 'mode'
+argument. This argument describes how you want to use the sources: whether
+you want to update and compile everything in the same tree (mode='update'),
+or do a fresh checkout and full build each time (mode='clobber'), or
+something in between.
+
+The SVN (Subversion) step has been unified and accepts the same mode=
+parameter as CVS. New version control steps will obey the same interface.
+
+Most of the old configuration arguments have been removed. You will need to
+update your configuration files to use the new arguments. See
+docs/steps.xhtml for a description of all the new parameters.
+
+
+*** Preliminary Debian packaging added
+
+Thanks to the contributions of Kirill Lapshin, we can now produce .deb
+installer packages. These are still experimental, but they include init.d
+startup/shutdown scripts, which the the new /usr/bin/buildbot to invoke
+twistd. Create your buildmasters in /var/lib/buildbot/master/FOO, and your
+slaves in /var/lib/buildbot/slave/BAR, then put FOO and BAR in the
+appropriate places in /etc/default/buildbot . After that, the buildmasters
+and slaves will be started at every boot.
+
+Pre-built .debs are not yet distributed. Use 'debuild -uc -us' from the
+source directory to create them.
+
+
+** minor features
+
+
+*** Source Stamps
+
+Each build now has a "source stamp" which describes what sources it used. The
+idea is that the sources for this particular build can be completely
+regenerated from the stamp. The stamp is a tuple of (revision, patch), where
+the revision depends on the VC system being used (for CVS it is either a
+revision tag like "BUILDBOT-0_5_0" or a datestamp like "2004/07/23", for
+Subversion it is a revision number like 11455). This must be combined with
+information from the Builder that is constant across all builds (something to
+point at the repository, and possibly a branch indicator for CVS and other VC
+systems that don't fold this into the repository string).
+
+The patch is an optional unified diff file, ready to be applied by running
+'patch -p0 <PATCH' from inside the workdir. This provides support for the
+'try' feature that will eventually allow developers to run buildbot tests on
+their code before checking it in.
+
+
+*** SIGHUP causes the buildmaster's configuration file to be re-read
+
+*** IRC bot now has 'watch' command
+
+You can now tell the buildbot's IRC bot to 'watch <buildername>' on a builder
+which is currently performing a build. When that build is finished, the
+buildbot will make an announcement (including the results of the build).
+
+The IRC 'force build' command will also announce when the resulting build has
+completed.
+
+
+*** the 'force build' option on HTML and IRC status targets can be disabled
+
+The html.Waterfall display and the words.IRC bot may be constructed with an
+allowForce=False argument, which removes the ability to force a build through
+these interfaces. Future versions will be able to restrict this build-forcing
+capability to authenticated users. The per-builder HTML page no longer
+displays the 'Force Build' buttons if it does not have this ability. Thanks
+to Fred Drake for code and design suggestions.
+
+
+*** master now takes 'projectName' and 'projectURL' settings
+
+These strings allow the buildbot to describe what project it is working for.
+At the moment they are only displayed on the Waterfall page, but in the next
+release they will be retrieveable from the IRC bot as well.
+
+
+*** survive recent (SVN) Twisted versions
+
+The buildbot should run correctly (albeit with plenty of noisy deprecation
+warnings) under the upcoming Twisted-2.0 release.
+
+
+*** work-in-progress realtime Trial results acquisition
+
+Jonathan Simms (<slyphon>) has been working on 'retrial', a rewrite of
+Twisted's unit test framework that will most likely be available in
+Twisted-2.0 . Although it is not yet complete, the buildbot will be able to
+use retrial in such a way that build status is reported on a per-test basis,
+in real time. This will be the beginning of fine-grained test tracking and
+Problem management, described in docs/users.xhtml .
+
+
+* Release 0.5.0 (22 Jul 2004)
+
+** new features
+
+*** web.distrib servers via TCP
+
+The 'webPathname' config option, which specifies a UNIX socket on which to
+publish the waterfall HTML page (for use by 'mktap web -u' or equivalent),
+now accepts a numeric port number. This publishes the same thing via TCP,
+allowing the parent web server to live on a separate machine.
+
+This config option could be named better, but it will go away altogether in
+a few releases, when status delivery is unified. It will be replaced with a
+WebStatusTarget object, and the config file will simply contain a list of
+various kinds of status targets.
+
+*** 'master.cfg' filename is configurable
+
+The buildmaster can use a config file named something other than
+"master.cfg". Use the --config=foo.cfg option to mktap to control this.
+
+*** FreshCVSSource now uses newcred (CVSToys >= 1.0.10)
+
+The FreshCVSSource class now defaults to speaking to freshcvs daemons from
+modern CVSToys releases. If you need to use the buildbot with a daemon from
+CVSToys-1.0.9 or earlier, use FreshCVSSourceOldcred instead. Note that the
+new form only requires host/port/username/passwd: the "serviceName"
+parameter is no longer meaningful.
+
+*** Builders are now configured with a dictionary, not a tuple
+
+The preferred way to set up a Builder in master.cfg is to provide a
+dictionary with various keys, rather than a (non-extensible) 4-tuple. See
+docs/config.xhtml for details. The old tuple-way is still supported for now,
+it will probably be deprecated in the next release and removed altogether in
+the following one.
+
+*** .periodicBuildTime is now exposed to the config file
+
+To set a builder to run at periodic intervals, simply add a
+'periodicBuildTime' key to its master.cfg dictionary. Again, see
+docs/config.xhtml for details.
+
+*** svn_buildbot.py adds --include, --exclude
+
+The commit trigger script now gives you more control over which files are
+sent to the buildmaster and which are not.
+
+*** usePTY is controllable at slave mktap time
+
+The buildslaves usually run their child processes in a pty, which creates a
+process group for all the children, which makes it much easier to kill them
+all at once (i.e. if a test hangs). However this causes problems on some
+systems. Rather than hacking slavecommand.py to disable the use of these
+ptys, you can now create the slave's .tap file with --usepty=0 at mktap
+time.
+
+** Twisted changes
+
+A summary of warnings (e.g. DeprecationWarnings) is provided as part of the
+test-case summarizer. The summarizer also counts Skips, expectedFailures,
+and unexpectedSuccesses, displaying the counts on the test step's event box.
+
+The RunUnitTests step now uses "trial -R twisted" instead of "trial
+twisted.test", which is a bit cleaner. All .pyc files are deleted before
+starting trial, to avoid getting tripped up by deleted .py files.
+
+** documentation
+
+docs/config.xhtml now describes the syntax and allowed contents of the
+'master.cfg' configuration file.
+
+** bugfixes
+
+Interlocks had a race condition that could cause the lock to get stuck
+forever.
+
+FreshCVSSource has a prefix= argument that was moderately broken (it used to
+only work if the prefix was a single directory component). It now works with
+subdirectories.
+
+The buildmaster used to complain when it saw the "info" directory in a
+slave's workspace. This directory is used to publish information about the
+slave host and its administrator, and is not a leftover build directory as
+the complaint suggested. This complain has been silenced.
+
+
+* Release 0.4.3 (30 Apr 2004)
+
+** PBChangeSource made explicit
+
+In 0.4.2 and before, an internal interface was available which allowed
+special clients to inject changes into the Buildmaster. This interface is
+used by the contrib/svn_buildbot.py script. The interface has been extracted
+into a proper PBChangeSource object, which should be created in the
+master.cfg file just like the other kinds of ChangeSources. See
+docs/sources.xhtml for details.
+
+If you were implicitly using this change source (for example, if you use
+Subversion and the svn_buildbot.py script), you *must* add this source to
+your master.cfg file, or changes will not be delivered and no builds will be
+triggered.
+
+The PBChangeSource accepts the same "prefix" argument as all other
+ChangeSources. For a SVN repository that follows the recommended practice of
+using "trunk/" for the trunk revisions, you probably want to construct the
+source like this:
+
+ source = PBChangeSource(prefix="trunk")
+
+to make sure that the Builders are given sensible (trunk-relative)
+filenames for each changed source file.
+
+** Twisted changes
+
+*** step_twisted.RunUnitTests can change "bin/trial"
+
+The twisted RunUnitTests step was enhanced to let you run something other
+than "bin/trial", making it easier to use a buildbot on projects which use
+Twisted but aren't actually Twisted itself.
+
+*** Twisted now uses Subversion
+
+Now that Twisted has moved from CVS to SVN, the Twisted build processes have
+been modified to perform source checkouts from the Subversion repository.
+
+** minor feature additions
+
+*** display Changes with HTML
+
+Changes are displayed with a bit more pizazz, and a links= argument was
+added to allow things like ViewCVS links to be added to the display
+(although it is not yet clear how this argument should be used: the
+interface remains subject to change untill it has been documented).
+
+*** display ShellCommand logs with HTML
+
+Headers are in blue, stderr is in red (unless usePTY=1 in which case stderr
+and stdout are indistinguishable). A link is provided which returns the same
+contents as plain text (by appending "?text=1" to the URL).
+
+*** buildslaves send real tracebacks upon error
+
+The .unsafeTracebacks option has been turned on for the buildslaves,
+allowing them to send a full stack trace when an exception occurs, which is
+logged in the buildmaster's twistd.log file. This makes it much easier to
+determine what went wrong on the slave side.
+
+*** BasicBuildFactory refactored
+
+The BasicBuildFactory class was refactored to make it easier to create
+derivative classes, in particular the BasicSVN variant.
+
+*** "ping buildslave" web button added
+
+There is now a button on the "builder information" page that lets a web user
+initiate a ping of the corresponding build slave (right next to the button
+that lets them force a build). This was added to help track down a problem
+with the slave keepalives.
+
+** bugs fixed:
+
+You can now have multiple BuildSteps with the same name (the names are used
+as hash keys in the data structure that helps determine ETA values for each
+step, the new code creates unique key names if necessary to avoid
+collisions). This means that, for example, you do not have to create a
+BuildStep subclass just to have two Compile steps in the same process.
+
+If CVSToys is not installed, the tests that depend upon it are skipped.
+
+Some tests in 0.4.2 failed because of a missing set of test files, they are
+now included in the tarball properly.
+
+Slave keepalives should work better now in the face of silent connection
+loss (such as when an intervening NAT box times out the association), the
+connection should be reestablished in minutes instead of hours.
+
+Shell commands on the slave are invoked with an argument list instead of the
+ugly and error-prone split-on-spaces approach. If the ShellCommand is given
+a string (instead of a list), it will fall back to splitting on spaces.
+Shell commands should work on win32 now (using COMSPEC instead of /bin/sh).
+
+Buildslaves under w32 should theoretically work now, and one was running for
+the Twisted buildbot for a while until the machine had to be returned.
+
+The "header" lines in ShellCommand logs (which include the first line, that
+displays the command being run, and the last, which shows its exit status)
+are now generated by the buildslave side instead of the local (buildmaster)
+side. This can provide better error handling and is generally cleaner.
+However, if you have an old buildslave (running 0.4.2 or earlier) and a new
+buildmaster, then neither end will generate these header lines.
+
+CVSCommand was improved, in certain situations 0.4.2 would perform
+unnecessary checkouts (when an update would have sufficed). Thanks to Johan
+Dahlin for the patches. The status output was fixed as well, so that
+failures in CVS and SVN commands (such as not being able to find the 'svn'
+executable) make the step status box red.
+
+Subversion support was refactored to make it behave more like CVS. This is a
+work in progress and will be improved in the next release.
+
+
+* Release 0.4.2 (08 Jan 2004)
+
+** test suite updated
+
+The test suite has been completely moved over to Twisted's "Trial"
+framework, and all tests now pass. To run the test suite (consisting of 64
+tests, probably covering about 30% of BuildBot's logic), do this:
+
+ PYTHONPATH=. trial -v buildbot.test
+
+** Mail parsers updated
+
+Several bugs in the mail-parsing code were fixed, allowing a buildmaster to
+be triggered by mail sent out by a CVS repository. (The Twisted Buildbot is
+now using this to trigger builds, as their CVS server machine is having some
+difficulties with FreshCVS). The FreshCVS mail format for directory
+additions appears to have changed recently: the new parser should handle
+both old and new-style messages.
+
+A parser for Bonsai commit messages (buildbot.changes.mail.parseBonsaiMail)
+was contributed by Stephen Davis. Thanks Stephen!
+
+** CVS "global options" now available
+
+The CVS build step can now accept a list of "global options" to give to the
+cvs command. These go before the "update"/"checkout" word, and are described
+fully by "cvs --help-options". Two useful ones might be "-r", which causes
+checked-out files to be read-only, and "-R", which assumes the repository is
+read-only (perhaps by not attempting to write to lock files).
+
+
+* Release 0.4.1 (09 Dec 2003)
+
+** MaildirSources fixed
+
+Several bugs in MaildirSource made them unusable. These have been fixed (for
+real this time). The Twisted buildbot is using an FCMaildirSource while they
+fix some FreshCVS daemon problems, which provided the encouragement for
+getting these bugs fixed.
+
+In addition, the use of DNotify (only available under linux) was somehow
+broken, possibly by changes in some recent version of Python. It appears to
+be working again now (against both python-2.3.3c1 and python-2.2.1).
+
+** master.cfg can use 'basedir' variable
+
+As documented in the sample configuration file (but not actually implemented
+until now), a variable named 'basedir' is inserted into the namespace used
+by master.cfg . This can be used with something like:
+
+  os.path.join(basedir, "maildir")
+
+to obtain a master-basedir-relative location.
+
+
+* Release 0.4.0 (05 Dec 2003)
+
+** newapp
+
+I've moved the codebase to Twisted's new 'application' framework, which
+drastically cleans up service startup/shutdown just like newcred did for
+authorization. This is mostly an internal change, but the interface to
+IChangeSources was modified, so in the off chance that someone has written a
+custom change source, it may have to be updated to the new scheme.
+
+The most user-visible consequence of this change is that now both
+buildmasters and buildslaves are generated with the standard Twisted 'mktap'
+utility. Basic documentation is in the README file.
+
+Both buildmaster and buildslave .tap files need to be re-generated to run
+under the new code. I have not figured out the styles.Versioned upgrade path
+well enough to avoid this yet. Sorry.
+
+This also means that both buildslaves and the buildmaster require
+Twisted-1.1.0 or later.
+
+** reloadable master.cfg
+
+Most aspects of a buildmaster is now controlled by a configuration file
+which can be re-read at runtime without losing build history. This feature
+makes the buildmaster *much* easier to maintain.
+
+In the previous release, you would create the buildmaster by writing a
+program to define the Builders and ChangeSources and such, then run it to
+create the .tap file. In the new release, you use 'mktap' to create the .tap
+file, and the only parameter you give it is the base directory to use. Each
+time the buildmaster starts, it will look for a file named 'master.cfg' in
+that directory and parse it as a python script. That script must define a
+dictionary named 'BuildmasterConfig' with various keys to define the
+builders, the known slaves, what port to use for the web server, what IRC
+channels to connect to, etc.
+
+This config file can be re-read at runtime, and the buildmaster will compute
+the differences and add/remove services as necessary. The re-reading is
+currently triggered through the debug port (contrib/debugclient.py is the
+debug port client), but future releases will add the ability to trigger the
+reconfiguration by IRC command, web page button, and probably a local UNIX
+socket (with a helper script to trigger a rebuild locally).
+
+docs/examples/twisted_master.cfg contains a sample configuration file, which
+also lists all the keys that can be set.
+
+There may be some bugs lurking, such as re-configuring the buildmaster while
+a build is running. It needs more testing.
+
+** MaxQ support
+
+Radix contributed some support scripts to run MaxQ test scripts. MaxQ
+(http://maxq.tigris.org/) is a web testing tool that allows you to record
+HTTP sessions and play them back.
+
+** Builders can now wait on multiple Interlocks
+
+The "Interlock" code has been enhanced to allow multiple builders to wait on
+each one. This was done to support the new config-file syntax for specifying
+Interlocks (in which each interlock is a tuple of A and [B], where A is the
+builder the Interlock depends upon, and [B] is a list of builders that
+depend upon the Interlock).
+
+"Interlock" is misnamed. In the next release it will be changed to
+"Dependency", because that's what it really expresses. A new class (probably
+called Interlock) will be created to express the notion that two builders
+should not run at the same time, useful when multiple builders are run on
+the same machine and thrashing results when several CPU- or disk- intensive
+compiles are done simultaneously.
+
+** FreshCVSSource can now handle newcred-enabled FreshCVS daemons
+
+There are now two FreshCVSSource classes: FreshCVSSourceNewcred talks to
+newcred daemons, and FreshCVSSourceOldcred talks to oldcred ones. Mind you,
+FreshCVS doesn't yet do newcred, but when it does, we'll be ready.
+
+'FreshCVSSource' maps to the oldcred form for now. That will probably change
+when the current release of CVSToys supports newcred by default.
+
+** usePTY=1 on posix buildslaves
+
+When a buildslave is running under POSIX (i.e. pretty much everything except
+windows), child processes are created with a pty instead of separate
+stdin/stdout/stderr pipes. This makes it more likely that a hanging build
+(when killed off by the timeout code) will have all its sub-childred cleaned
+up. Non-pty children would tend to leave subprocesses running because the
+buildslave was only able to kill off the top-level process (typically
+'make').
+
+Windows doesn't have any concept of ptys, so non-posix systems do not try to
+enable them.
+
+** mail parsers should actually work now
+
+The email parsing functions (FCMaildirSource and SyncmailMaildirSource) were
+broken because of my confused understanding of how python class methods
+work. These sources should be functional now.
+
+** more irc bot sillyness
+
+The IRC bot can now perform half of the famous AYBABTO scene.
+
+
+* Release 0.3.5 (19 Sep 2003)
+
+** newcred
+
+Buildbot has moved to "newcred", a new authorization framework provided by
+Twisted, which is a good bit cleaner and easier to work with than the
+"oldcred" scheme in older versions. This causes both buildmaster and
+buildslaves to depend upon Twisted 1.0.7 or later. The interface to
+'makeApp' has changed somewhat (the multiple kinds of remote connections all
+use the same TCP port now).
+
+Old buildslaves will get "_PortalWrapper instance has no attribute
+'remote_username'" errors when they try to connect. They must be upgraded.
+
+The FreshCVSSource uses PB to connect to the CVSToys server. This has been
+upgraded to use newcred too. If you get errors (TODO: what do they look
+like?) in the log when the buildmaster tries to connect, you need to upgrade
+your FreshCVS service or use the 'useOldcred' argument when creating your
+FreshCVSSource. This is a temporary hack to allow the buildmaster to talk to
+oldcred CVSToys servers. Using it will trigger deprecation warnings. It will
+go away eventually.
+
+In conjunction with this change, makeApp() now accepts a password which can
+be applied to the debug service.
+
+** new features
+
+*** "copydir" for CVS checkouts
+
+The CVS build step can now accept a "copydir" parameter, which should be a
+directory name like "source" or "orig". If provided, the CVS checkout is
+done once into this directory, then copied into the actual working directory
+for compilation etc. Later updates are done in place in the copydir, then
+the workdir is replaced with a copy.
+
+This reduces CVS bandwidth (update instead of full checkout) at the expense
+of twice the disk space (two copies of the tree).
+
+*** Subversion (SVN) support
+
+Radix (Christopher Armstrong) contributed early support for building
+Subversion-based trees. The new 'SVN' buildstep behaves roughly like the
+'CVS' buildstep, and the contrib/svn_buildbot.py script can be used as a
+checkin trigger to feed changes to a running buildmaster.
+
+** notable bugfixes
+
+*** .tap file generation
+
+We no longer set the .tap filename, because the buildmaster/buildslave
+service might be added to an existing .tap file and we shouldn't presume to
+own the whole thing. You may want to manually rename the "buildbot.tap" file
+to something more meaningful (like "buildslave-bot1.tap").
+
+*** IRC reconnect
+
+If the IRC server goes away (it was restarted, or the network connection was
+lost), the buildmaster will now schedule a reconnect attempt.
+
+*** w32 buildslave fixes
+
+An "rm -rf" was turned into shutil.rmtree on non-posix systems.
+
+
+* Release 0.3.4 (28 Jul 2003)
+
+** IRC client
+
+The buildmaster can now join a set of IRC channels and respond to simple
+queries about builder status.
+
+** slave information
+
+The build slaves can now report information from a set of info/* files in
+the slave base directory to the buildmaster. This will be used by the slave
+administrator to announce details about the system hosting the slave,
+contact information, etc. For now, info/admin should contain the name/email
+of the person who is responsible for the buildslave, and info/host should
+describe the system hosting the build slave (OS version, CPU speed, memory,
+etc). The contents of these files are made available through the waterfall
+display.
+
+** change notification email parsers
+
+A parser for Syncmail (syncmail.sourceforge.net) was added. SourceForge
+provides examples of setting up syncmail to deliver CVS commit messages to
+mailing lists, so hopefully this will make it easier for sourceforge-hosted
+projects to set up a buildbot.
+
+email processors were moved into buildbot.changes.mail . FCMaildirSource was
+moved, and the compatibility location (buildbot.changes.freshcvsmail) will
+go away in the next release.
+
+** w32 buildslave ought to work
+
+Some non-portable code was changed to make it more likely that the
+buildslave will run under windows. The Twisted buildbot now has a
+(more-or-less) working w32 buildslave.
+
+
+* Release 0.3.3 (21 May 2003):
+
+** packaging changes
+
+*** include doc/examples in the release. Oops again.
+
+** network changes
+
+*** add keepalives to deal with NAT boxes
+
+Some NAT boxes drop port mappings if the TCP connection looks idle for too
+long (maybe 30 minutes?). Add application-level keepalives (dummy commands
+sent from slave to master every 10 minutes) to appease the NAT box and keep
+our connection alive. Enable this with --keepalive in the slave mktap
+command line. Check the README for more details.
+
+** UI changes
+
+*** allow slaves to trigger any build that they host
+
+Added an internal function to ask the buildmaster to start one of their
+builds. Must be triggered with a debugger or manhole on the slave side for
+now, will add a better UI later.
+
+*** allow web page viewers to trigger any build
+
+Added a button to the per-build page (linked by the build names on the third
+row of the waterfall page) to allow viewers to manually trigger builds.
+There is a field for them to indicate who they are and why they are
+triggering the build. It is possible to abuse this, but for now the benefits
+outweigh the damage that could be done (worst case, someone can make your
+machine run builds continuously).
+
+** generic buildprocess changes
+
+*** don't queue multiple builds for offline slaves
+
+If a slave is not online when a build is ready to run, that build is queued
+so the slave will run it when it next connects. However, the buildmaster
+used to queue every such build, so the poor slave machine would be subject
+to tens or hundreds of builds in a row when they finally did come online.
+The buildmaster has been changed to merge these multiple builds into a
+single one.
+
+*** bump ShellCommand default timeout to 20 minutes
+
+Used for testing out the win32 twisted builder. I will probably revert this
+in the next relese.
+
+*** split args in ShellCommand ourselves instead of using /bin/sh
+
+This should remove the need for /bin/sh on the slave side, improving the
+chances that the buildslave can run on win32.
+
+*** add configureEnv argument to Configure step, pass env dict to slave
+
+Allows build processes to do things like 'CFLAGS=-O0 ./configure' without
+using /bin/sh to set the environment variable
+
+** Twisted buildprocess changes
+
+*** warn instead of flunk the build when cReactor or qtreactor tests fail
+
+These two always fail. For now, downgrade those failures to a warning
+(orange box instead of red).
+
+*** don't use 'clobber' on remote builds
+
+Builds that run on remote machines (freebsd, OS-X) now use 'cvs update'
+instead of clobbering their trees and doing a fresh checkout. The multiple
+simultaneous CVS checkouts were causing a strain on Glyph's upstream
+bandwidth.
+
+*** use trial --testmodule instead of our own test-case-name grepper
+
+The Twisted coding/testing convention has developers put 'test-case-name'
+tags (emacs local variables, actually) in source files to indicate which
+test cases should be run to exercise that code. Twisted's unit-test
+framework just acquired an argument to look for these tags itself. Use that
+instead of the extra FindUnitTestsForFiles build step we were doing before.
+Removes a good bit of code from buildbot and into Twisted where it really
+belongs.
+
+
+* Release 0.3.2 (07 May 2003):
+
+** packaging changes
+
+*** fix major packaging bug: none of the buildbot/* subdirectories were
+included in the 0.3.1 release. Sorry, I'm still figuring out distutils
+here..
+
+** internal changes
+
+*** use pb.Cacheable to update Events in remote status client. much cleaner.
+
+*** start to clean up BuildProcess->status.builder interface
+
+** bug fixes
+
+*** waterfall display was missing a <tr>, causing it to be misrendered in most
+browsers (except the one I was testing it with, of course)
+
+*** URL without trailing slash (when served in a twisted-web distributed
+server, with a url like "http://twistedmatrix.com/~warner.twistd") should do
+redirect to URL-with-trailing-slash, otherwise internal hrefs are broken.
+
+*** remote status clients: forget RemoteReferences at shutdown, removes
+warnings about "persisting Ephemerals"
+
+** Twisted buildprocess updates:
+
+*** match build process as of twisted-1.0.5
+**** use python2.2 everywhere now that twisted rejects python2.1
+**** look for test-result constants in multiple places
+*** move experimental 'trial --jelly' code to separate module
+*** add FreeBSD builder
+*** catch rc!=0 in HLint step
+*** remove RunUnitTestsRandomly, use randomly=1 parameter instead
+*** parameterize ['twisted.test'] default test case to make subclassing easier
+*** ignore internal distutils warnings in python2.3 builder
+
+
+* Release 0.3.1 (29 Apr 2003):
+
+** First release.
+
+** Features implemented:
+
+ change notification from FreshCVS server or parsed maildir contents
+
+ timed builds
+
+ basic builds, configure/compile/test
+
+ some Twisted-specific build steps: docs, unit tests, debuild
+
+ status reporting via web page
+
+** Features still experimental/unpolished
+
+ status reporting via PB client
new file mode 100644
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,30 @@
+Metadata-Version: 1.0
+Name: buildbot
+Version: 0.7.9
+Summary: BuildBot build automation system
+Home-page: http://buildbot.net/
+Author: Brian Warner
+Author-email: warner-buildbot@lothar.com
+License: GNU GPL
+Description: 
+        The BuildBot is a system to automate the compile/test cycle required by
+        most software projects to validate code changes. By automatically
+        rebuilding and testing the tree each time something has changed, build
+        problems are pinpointed quickly, before other developers are
+        inconvenienced by the failure. The guilty developer can be identified
+        and harassed without human intervention. By running the builds on a
+        variety of platforms, developers who do not have the facilities to test
+        their changes everywhere before checkin will at least know shortly
+        afterwards whether they have broken the build or not. Warning counts,
+        lint checks, image size, compile time, and other build parameters can
+        be tracked over time, are more visible, and are therefore easier to
+        improve.
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: No Input/Output (Daemon)
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU General Public License (GPL)
+Classifier: Topic :: Software Development :: Build Tools
+Classifier: Topic :: Software Development :: Testing
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,201 @@
+
+BuildBot: build/test automation
+  http://buildbot.net
+  Brian Warner <warner-buildbot @ lothar . com>
+
+
+Abstract:
+
+The BuildBot is a system to automate the compile/test cycle required by most
+software projects to validate code changes. By automatically rebuilding and
+testing the tree each time something has changed, build problems are
+pinpointed quickly, before other developers are inconvenienced by the
+failure. The guilty developer can be identified and harassed without human
+intervention. By running the builds on a variety of platforms, developers
+who do not have the facilities to test their changes everywhere before
+checkin will at least know shortly afterwards whether they have broken the
+build or not. Warning counts, lint checks, image size, compile time, and
+other build parameters can be tracked over time, are more visible, and
+are therefore easier to improve.
+
+The overall goal is to reduce tree breakage and provide a platform to run
+tests or code-quality checks that are too annoying or pedantic for any human
+to waste their time with. Developers get immediate (and potentially public)
+feedback about their changes, encouraging them to be more careful about
+testing before checkin.
+
+
+Features:
+
+ * run builds on a variety of slave platforms
+ * arbitrary build process: handles projects using C, Python, whatever
+ * minimal host requirements: python and Twisted
+ * slaves can be behind a firewall if they can still do checkout
+ * status delivery through web page, email, IRC, other protocols
+ * track builds in progress, provide estimated completion time
+ * flexible configuration by subclassing generic build process classes
+ * debug tools to force a new build, submit fake Changes, query slave status
+ * released under the GPL
+
+
+DOCUMENTATION:
+
+The PyCon paper has a good description of the overall architecture. It is
+available in HTML form in docs/PyCon-2003/buildbot.html, or on the web page.
+
+The User's Manual is in docs/buildbot.info, and the Installation chapter is
+the best guide to use for setup instructions. The .texinfo source can also be
+turned into printed documentation. An HTML representation is available on the
+Buildbot home page.
+
+REQUIREMENTS:
+
+ Python: http://www.python.org
+
+   Buildbot requires python-2.3 or later, and is primarily developed against
+   python-2.4 . It is also tested against python-2.5 .
+
+ Twisted: http://twistedmatrix.com
+
+   Both the buildmaster and the buildslaves require Twisted-2.0.x or later.
+   As always, the most recent version is recommended. It has been tested
+   against Twisted-2.5.0, Twisted-8.0.1, Twisted-8.1.0, and Twisted SVN as of
+   the date of release.
+
+   Certain versions of Twisted are delivered as a collection of subpackages.
+   You'll need at least "Twisted" (the core package), and you'll also want
+   TwistedMail, TwistedWeb, and TwistedWords (for sending email, serving a
+   web status page, and delivering build status via IRC, respectively). You
+   might also want TwistedConch (for the encrypted Manhole debug port). Note
+   that Twisted requires ZopeInterface to be installed as well.
+
+INSTALLATION:
+
+Please read the User's Manual in docs/buildbot.info or docs/buildbot.html for
+complete instructions. This file only contains a brief summary.
+
+ RUNNING THE UNIT TESTS
+
+If you would like to run the unit test suite, use a command like this:
+
+ PYTHONPATH=. trial buildbot.test
+
+This should run up to 221 tests, depending upon what VC tools you have
+installed. On my desktop machine it takes about six minutes to complete.
+Nothing should fail (at least under unix), a few might be skipped. If any of
+the tests fail, you should stop and investigate the cause before continuing
+the installation process, as it will probably be easier to track down the bug
+early. There are a few known failures under windows and OS-X, but please
+report these to the mailing list so we can isolate and resolve them.
+
+Neither CVS nor SVN support file based repositories on network filesystem
+(or network drives in Windows parlance). Therefore it is recommended to run
+all unit tests on local hard disks.
+
+ INSTALLING THE LIBRARIES:
+
+The first step is to install the python libraries. This package uses the
+standard 'distutils' module, so installing them is usually a matter of
+doing something like:
+
+ python ./setup.py install
+
+To test this, shift to a different directory (like /tmp), and run:
+
+ buildbot --version
+
+If it announces the versions of Buildbot and Twisted, the install went ok.
+
+
+ SETTING UP A BUILD SLAVE:
+
+If you want to run a build slave, you need to obtain the following pieces of
+information from the administrator of the buildmaster you intend to connect
+to:
+
+ your buildslave's name
+ the password assigned to your buildslave
+ the hostname and port number of the buildmaster, i.e. example.com:8007
+
+You also need to pick a working directory for the buildslave. All commands
+will be run inside this directory.
+
+Now run the 'buildbot' command as follows:
+
+ buildbot create-slave WORKDIR MASTERHOST:PORT SLAVENAME PASSWORD
+
+This will create a file called "buildbot.tac", which bundles up all the state
+needed by the build slave application. Twisted has a tool called "twistd"
+which knows how to load these saved applications and start running them.
+twistd takes care of logging and daemonization (running the program in the
+background). /usr/bin/buildbot is a front end which runs twistd for you.
+
+Once you've set up the directory with the .tac file, you start it running
+like this:
+
+ buildbot start WORKDIR
+
+This will start the build slave in the background and finish, so you don't
+need to put it in the background yourself with "&". The process ID of the
+background task is written to a file called "twistd.pid", and all output from
+the program is written to a log file named "twistd.log". Look in twistd.log
+to make sure the buildslave has started.
+
+To shut down the build slave, use:
+
+ buildbot stop WORKDIR
+
+
+ RUNNING BEHIND A NAT BOX:
+
+Some network environments will not properly maintain a TCP connection that
+appears to be idle. NAT boxes which do some form of connection tracking may
+drop the port mapping if it looks like the TCP session has been idle for too
+long. The buildslave attempts to turn on TCP "keepalives" (supported by
+Twisted 1.0.6 and later), and if these cannot be activated, it uses
+application level keepalives (which send a dummy message to the build master
+on a periodic basis). The TCP keepalive is typically sent at intervals of
+about 2 hours, and is configurable through the kernel. The application-level
+keepalive defaults to running once every 10 minutes.
+
+To manually turn on application-level keepalives, or to set them to use some
+other interval, add "--keepalive NNN" to the 'buildbot slave' command line.
+NNN is the number of seconds between keepalives. Use as large a value as your
+NAT box allows to reduce the amount of unnecessary traffic on the wire. 600
+seconds (10 minutes) is a reasonable value.
+
+
+ SETTING UP A BUILD MASTER:
+
+Please read the user's manual for instructions. The short form is that you
+use 'buildbot create-master MASTERDIR' to create the base directory, then you
+edit the 'master.cfg' file to configure the buildmaster. Once this is ready,
+you use 'buildbot start MASTERDIR' to launch it.
+
+A sample configuration file will be created for you in WORKDIR/master.cfg .
+There are more examples in docs/examples/, and plenty of documentation in the
+user's manual. Everything is controlled by the config file.
+
+
+SUPPORT:
+
+ Please send questions, bugs, patches, etc, to the buildbot-devel mailing
+ list reachable through http://buildbot.net/, so that everyone can see them.
+
+
+COPYING:
+
+    Buildbot is free software: you can redistribute it and/or modify it under
+    the terms of the GNU General Public License as published by the Free
+    Software Foundation, version 2.
+
+    This program is distributed in the hope that it will be useful, but
+    WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+    Public License for more details.
+
+    For full details, please see the file named COPYING in the top directory
+    of the source tree. You should have received a copy of the GNU General
+    Public License along with this program. If not, see
+    <http://www.gnu.org/licenses/>.
+
new file mode 100644
--- /dev/null
+++ b/README.w32
@@ -0,0 +1,95 @@
+Several users have reported success in running a buildslave under Windows.
+The following list of steps might help you accomplish the same. They are a
+list of what I did as a unix guy struggling to make a winXP box run the
+buildbot unit tests. When I was done, most of the unit tests passed.
+
+If you discover things that are missing or incorrect, please send your
+corrections to the buildbot-devel mailing list (archives and subscription
+information are available at http://buildbot.sourceforge.net).
+
+Many thanks to Mike "Bear" Taylor for developing this list.
+
+
+0. Check to make sure your PATHEXT environment variable has ";.PY" in 
+it -- if not set your global environment to include it.
+
+ Control Panels / System / Advanced / Environment Variables / System variables
+
+1. Install python -- 2.4 -- http://python.org
+	* run win32 installer - no special options needed so far
+
+2. install zope interface package -- 3.0.1final -- 
+http://www.zope.org/Products/ZopeInterface
+	* run win32 installer - it should auto-detect your python 2.4
+          installation
+
+3. python for windows extensions -- build 203 -- 
+http://pywin32.sourceforge.net/
+	* run win32 installer - it should auto-detect your python 2.4 
+          installation
+
+ the installer complains about a missing DLL. Download mfc71.dll from the
+ site mentioned in the warning
+ (http://starship.python.net/crew/mhammond/win32/) and move it into
+ c:\Python24\DLLs
+
+4. at this point, to preserve my own sanity, I grabbed cygwin.com's setup.exe
+   and started it. It behaves a lot like dselect. I installed bash and other
+   tools (but *not* python). I added C:\cygwin\bin to PATH, allowing me to
+   use tar, md5sum, cvs, all the usual stuff. I also installed emacs, going
+   from the notes at http://www.gnu.org/software/emacs/windows/ntemacs.html .
+   Their FAQ at http://www.gnu.org/software/emacs/windows/faq3.html#install
+   has a note on how to swap CapsLock and Control.
+
+ I also modified PATH (in the same place as PATHEXT) to include C:\Python24
+ and C:\Python24\Scripts . This will allow 'python' and (eventually) 'trial'
+ to work in a regular command shell.
+
+5. twisted -- 2.0 -- http://twistedmatrix.com/projects/core/
+	* unpack tarball and run
+		python setup.py install
+	Note: if you want to test your setup - run:
+		python c:\python24\Scripts\trial.py -o -R twisted
+	(the -o will format the output for console and the "-R twisted" will 
+         recursively run all unit tests)
+
+ I had to edit Twisted (core)'s setup.py, to make detectExtensions() return
+ an empty list before running builder._compile_helper(). Apparently the test
+ it uses to detect if the (optional) C modules can be compiled causes the
+ install process to simply quit without actually installing anything.
+
+ I installed several packages: core, Lore, Mail, Web, and Words. They all got
+ copied to C:\Python24\Lib\site-packages\
+
+ At this point
+
+   trial --version
+
+ works, so 'trial -o -R twisted' will run the Twisted test suite. Note that
+ this is not necessarily setting PYTHONPATH, so it may be running the test
+ suite that was installed, not the one in the current directory.
+
+6. I used CVS to grab a copy of the latest Buildbot sources. To run the
+   tests, you must first add the buildbot directory to PYTHONPATH. Windows
+   does not appear to have a Bourne-shell-style syntax to set a variable just
+   for a single command, so you have to set it once and remember it will
+   affect all commands for the lifetime of that shell session.
+
+  set PYTHONPATH=.
+  trial -o -r win32 buildbot.test
+
+ To run against both buildbot-CVS and, say, Twisted-SVN, do:
+
+  set PYTHONPATH=.;C:\path to\Twisted-SVN
+
+
+All commands are done using the normal cmd.exe command shell. As of
+buildbot-0.6.4, only one unit test fails (test_webPathname_port) when you run
+under the 'win32' reactor. (if you run under the default reactor, many of the
+child-process-spawning commands fail, but test_webPathname_port passes. go
+figure.)
+
+Actually setting up a buildslave is not yet covered by this document. Patches
+gladly accepted.
+
+ -Brian
new file mode 100755
--- /dev/null
+++ b/bin/buildbot
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+
+from buildbot.scripts import runner
+runner.run()
new file mode 100644
--- /dev/null
+++ b/buildbot.egg-info/PKG-INFO
@@ -0,0 +1,30 @@
+Metadata-Version: 1.0
+Name: buildbot
+Version: 0.7.9
+Summary: BuildBot build automation system
+Home-page: http://buildbot.net/
+Author: Brian Warner
+Author-email: warner-buildbot@lothar.com
+License: GNU GPL
+Description: 
+        The BuildBot is a system to automate the compile/test cycle required by
+        most software projects to validate code changes. By automatically
+        rebuilding and testing the tree each time something has changed, build
+        problems are pinpointed quickly, before other developers are
+        inconvenienced by the failure. The guilty developer can be identified
+        and harassed without human intervention. By running the builds on a
+        variety of platforms, developers who do not have the facilities to test
+        their changes everywhere before checkin will at least know shortly
+        afterwards whether they have broken the build or not. Warning counts,
+        lint checks, image size, compile time, and other build parameters can
+        be tracked over time, are more visible, and are therefore easier to
+        improve.
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: No Input/Output (Daemon)
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU General Public License (GPL)
+Classifier: Topic :: Software Development :: Build Tools
+Classifier: Topic :: Software Development :: Testing
new file mode 100644
--- /dev/null
+++ b/buildbot.egg-info/SOURCES.txt
@@ -0,0 +1,200 @@
+COPYING
+CREDITS
+MANIFEST.in
+NEWS
+README
+README.w32
+setup.py
+bin/buildbot
+buildbot/__init__.py
+buildbot/buildbot.png
+buildbot/buildset.py
+buildbot/buildslave.py
+buildbot/dnotify.py
+buildbot/interfaces.py
+buildbot/locks.py
+buildbot/manhole.py
+buildbot/master.py
+buildbot/pbutil.py
+buildbot/scheduler.py
+buildbot/sourcestamp.py
+buildbot/util.py
+buildbot.egg-info/PKG-INFO
+buildbot.egg-info/SOURCES.txt
+buildbot.egg-info/dependency_links.txt
+buildbot.egg-info/requires.txt
+buildbot.egg-info/top_level.txt
+buildbot/changes/__init__.py
+buildbot/changes/base.py
+buildbot/changes/bonsaipoller.py
+buildbot/changes/changes.py
+buildbot/changes/dnotify.py
+buildbot/changes/freshcvs.py
+buildbot/changes/hgbuildbot.py
+buildbot/changes/mail.py
+buildbot/changes/maildir.py
+buildbot/changes/monotone.py
+buildbot/changes/p4poller.py
+buildbot/changes/pb.py
+buildbot/changes/svnpoller.py
+buildbot/clients/__init__.py
+buildbot/clients/base.py
+buildbot/clients/debug.glade
+buildbot/clients/debug.py
+buildbot/clients/gtkPanes.py
+buildbot/clients/sendchange.py
+buildbot/process/__init__.py
+buildbot/process/base.py
+buildbot/process/builder.py
+buildbot/process/buildstep.py
+buildbot/process/factory.py
+buildbot/process/process_twisted.py
+buildbot/process/properties.py
+buildbot/process/step_twisted2.py
+buildbot/scripts/__init__.py
+buildbot/scripts/checkconfig.py
+buildbot/scripts/logwatcher.py
+buildbot/scripts/reconfig.py
+buildbot/scripts/runner.py
+buildbot/scripts/sample.cfg
+buildbot/scripts/startup.py
+buildbot/scripts/tryclient.py
+buildbot/slave/__init__.py
+buildbot/slave/bot.py
+buildbot/slave/commands.py
+buildbot/slave/interfaces.py
+buildbot/slave/registry.py
+buildbot/status/__init__.py
+buildbot/status/base.py
+buildbot/status/builder.py
+buildbot/status/client.py
+buildbot/status/html.py
+buildbot/status/mail.py
+buildbot/status/progress.py
+buildbot/status/tests.py
+buildbot/status/tinderbox.py
+buildbot/status/words.py
+buildbot/status/web/__init__.py
+buildbot/status/web/about.py
+buildbot/status/web/base.py
+buildbot/status/web/baseweb.py
+buildbot/status/web/build.py
+buildbot/status/web/builder.py
+buildbot/status/web/changes.py
+buildbot/status/web/classic.css
+buildbot/status/web/grid.py
+buildbot/status/web/index.html
+buildbot/status/web/logs.py
+buildbot/status/web/robots.txt
+buildbot/status/web/slaves.py
+buildbot/status/web/step.py
+buildbot/status/web/tests.py
+buildbot/status/web/waterfall.py
+buildbot/status/web/xmlrpc.py
+buildbot/steps/__init__.py
+buildbot/steps/dummy.py
+buildbot/steps/maxq.py
+buildbot/steps/python.py
+buildbot/steps/python_twisted.py
+buildbot/steps/shell.py
+buildbot/steps/source.py
+buildbot/steps/transfer.py
+buildbot/steps/trigger.py
+buildbot/test/__init__.py
+buildbot/test/emit.py
+buildbot/test/emitlogs.py
+buildbot/test/runutils.py
+buildbot/test/sleep.py
+buildbot/test/test__versions.py
+buildbot/test/test_bonsaipoller.py
+buildbot/test/test_buildreq.py
+buildbot/test/test_buildstep.py
+buildbot/test/test_changes.py
+buildbot/test/test_config.py
+buildbot/test/test_control.py
+buildbot/test/test_dependencies.py
+buildbot/test/test_locks.py
+buildbot/test/test_maildir.py
+buildbot/test/test_mailparse.py
+buildbot/test/test_p4poller.py
+buildbot/test/test_properties.py
+buildbot/test/test_run.py
+buildbot/test/test_runner.py
+buildbot/test/test_scheduler.py
+buildbot/test/test_shell.py
+buildbot/test/test_slavecommand.py
+buildbot/test/test_slaves.py
+buildbot/test/test_status.py
+buildbot/test/test_steps.py
+buildbot/test/test_svnpoller.py
+buildbot/test/test_transfer.py
+buildbot/test/test_twisted.py
+buildbot/test/test_util.py
+buildbot/test/test_vc.py
+buildbot/test/test_web.py
+buildbot/test/test_webparts.py
+buildbot/test/mail/freshcvs.1
+buildbot/test/mail/freshcvs.2
+buildbot/test/mail/freshcvs.3
+buildbot/test/mail/freshcvs.4
+buildbot/test/mail/freshcvs.5
+buildbot/test/mail/freshcvs.6
+buildbot/test/mail/freshcvs.7
+buildbot/test/mail/freshcvs.8
+buildbot/test/mail/freshcvs.9
+buildbot/test/mail/svn-commit.1
+buildbot/test/mail/svn-commit.2
+buildbot/test/mail/syncmail.1
+buildbot/test/mail/syncmail.2
+buildbot/test/mail/syncmail.3
+buildbot/test/mail/syncmail.4
+buildbot/test/mail/syncmail.5
+buildbot/test/subdir/emit.py
+contrib/README.txt
+contrib/arch_buildbot.py
+contrib/bb_applet.py
+contrib/darcs_buildbot.py
+contrib/fakechange.py
+contrib/git_buildbot.py
+contrib/hg_buildbot.py
+contrib/run_maxq.py
+contrib/svn_buildbot.py
+contrib/svn_watcher.py
+contrib/svnpoller.py
+contrib/viewcvspoll.py
+contrib/CSS/sample1.css
+contrib/CSS/sample2.css
+contrib/OS-X/README
+contrib/OS-X/net.sourceforge.buildbot.master.plist
+contrib/OS-X/net.sourceforge.buildbot.slave.plist
+contrib/windows/buildbot.bat
+contrib/windows/buildbot2.bat
+contrib/windows/buildbot_service.py
+contrib/windows/setup.py
+docs/buildbot.html
+docs/buildbot.info
+docs/buildbot.info-1
+docs/buildbot.info-2
+docs/buildbot.texinfo
+docs/epyrun
+docs/gen-reference
+docs/hexnut32.png
+docs/hexnut48.png
+docs/hexnut64.png
+docs/examples/hello.cfg
+docs/examples/twisted_master.cfg
+docs/images/master.png
+docs/images/master.svg
+docs/images/master.txt
+docs/images/overview.png
+docs/images/overview.svg
+docs/images/overview.txt
+docs/images/slavebuilder.png
+docs/images/slavebuilder.svg
+docs/images/slavebuilder.txt
+docs/images/slaves.png
+docs/images/slaves.svg
+docs/images/slaves.txt
+docs/images/status.png
+docs/images/status.svg
+docs/images/status.txt
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/buildbot.egg-info/dependency_links.txt
@@ -0,0 +1,1 @@
+
new file mode 100644
--- /dev/null
+++ b/buildbot.egg-info/requires.txt
@@ -0,0 +1,1 @@
+twisted >= 2.0.0
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/buildbot.egg-info/top_level.txt
@@ -0,0 +1,1 @@
+buildbot
new file mode 100644
--- /dev/null
+++ b/buildbot/__init__.py
@@ -0,0 +1,2 @@
+
+version = "0.7.9"
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..387ba15f4c574716a5a1405ed83a44ec0d9aa292
GIT binary patch
literal 783
zc$@(b1MvKbP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00009a7bBm000XU
z000XU0RWnu7ytkO2XskIMF-Uc8vz^%V%FZQ00086Nkl<ZILoDz&uiRO6vaRHzW4op
z=LZd)B!-HU2Ab4`DHM%LEp%fMYtfC1x>FaDO%azaD%3x~O*gJ|Bcg&rKnrTcg@`f1
zVuT{4H7)5RO<HL>%_Pn=^L_7K7p0|CcRj1a;cz(z&Xs`!)51eQc}V#G6LT?+jPIxn
z57Zm|?cSha4X4g#SNpSz>s`CwEB9A8{#<jfVqcosgqhWn0T{`Q7!zjv7GgL#bH4pu
zr+nbRyN4ftMzGT{rbi42Qe|xSI3t5YcnP$YFR`@JBBew!n>FL)$7ff6bVYMY-h1(h
zBSn^-7S)Dxp0!R%&>A5Ldv=d8(r8f6jIwu&UJz8>sB`Z1r<*$`-3P~~-^hISnfJb`
zqLgLe%Jp@s)jBIzFEjJar(A2DqwL+pQlLzZEUO@@sH*tfyJMn0i!qY<oGdH&=hgz>
zfBVV$jT>v91IzPse)6@WuN-@F-~LKg)B(Yf5o7H8<<^Dwo1=SQa;^}=@ZNLy;31xx
z+;?WMG4fSWskS<soii5~|Lj{a{8sLB%#0W#&iN<<2d3qnXB%Jdnb`lbh$E&*N(m6k
z7{CA#R2?BiQi>!qK!_>LE5Oq7(uuXzOMNg@lsqd?b>vw=p5NhShN$4Y$NP*t&r#LV
z70oHRuvPhcRIaDtp<U1Wyo!-f=P(2nLDZotsPlk;dO`wR@rx^(Qvy)8tzWaS`NFP8
z$C`OjC8b0^ByN|H?slMC8k@bqR%vwmiS20ZZkb-aH|6<_MtE#T`^~==&;4E*oT!x{
za(9U$dZ&UYX6Y_}`_vC}SMMbNINvGO$BT^(R~tGSLc}Z)V<d!<lv2Cj-<}IG&Xi?&
z>Z70k_#&G9_nFRK>wkD~XZw)LYjst(5R)yK*}_j3SFT*Xxn&Rf{{?RcNrh~FBa#3B
N002ovPDHLkV1iDnbd>-A
new file mode 100644
--- /dev/null
+++ b/buildbot/buildset.py
@@ -0,0 +1,81 @@
+from buildbot.process import base
+from buildbot.status import builder
+from buildbot.process.properties import Properties
+
+
+class BuildSet:
+    """I represent a set of potential Builds, all of the same source tree,
+    across a specified list of Builders. I can represent a build of a
+    specific version of the source tree (named by source.branch and
+    source.revision), or a build of a certain set of Changes
+    (source.changes=list)."""
+
+    def __init__(self, builderNames, source, reason=None, bsid=None,
+                 properties=None):
+        """
+        @param source: a L{buildbot.sourcestamp.SourceStamp}
+        """
+        self.builderNames = builderNames
+        self.source = source
+        self.reason = reason
+
+        self.properties = Properties()
+        if properties: self.properties.updateFromProperties(properties)
+
+        self.stillHopeful = True
+        self.status = bss = builder.BuildSetStatus(source, reason,
+                                                   builderNames, bsid)
+
+    def waitUntilSuccess(self):
+        return self.status.waitUntilSuccess()
+    def waitUntilFinished(self):
+        return self.status.waitUntilFinished()
+
+    def start(self, builders):
+        """This is called by the BuildMaster to actually create and submit
+        the BuildRequests."""
+        self.requests = []
+        reqs = []
+
+        # create the requests
+        for b in builders:
+            req = base.BuildRequest(self.reason, self.source, b.name, 
+                                    properties=self.properties)
+            reqs.append((b, req))
+            self.requests.append(req)
+            d = req.waitUntilFinished()
+            d.addCallback(self.requestFinished, req)
+
+        # tell our status about them
+        req_statuses = [req.status for req in self.requests]
+        self.status.setBuildRequestStatuses(req_statuses)
+
+        # now submit them
+        for b,req in reqs:
+            b.submitBuildRequest(req)
+
+    def requestFinished(self, buildstatus, req):
+        # TODO: this is where individual build status results are aggregated
+        # into a BuildSet-wide status. Consider making a rule that says one
+        # WARNINGS results in the overall status being WARNINGS too. The
+        # current rule is that any FAILURE means FAILURE, otherwise you get
+        # SUCCESS.
+        self.requests.remove(req)
+        results = buildstatus.getResults()
+        if results == builder.FAILURE:
+            self.status.setResults(results)
+            if self.stillHopeful:
+                # oh, cruel reality cuts deep. no joy for you. This is the
+                # first failure. This flunks the overall BuildSet, so we can
+                # notify success watchers that they aren't going to be happy.
+                self.stillHopeful = False
+                self.status.giveUpHope()
+                self.status.notifySuccessWatchers()
+        if not self.requests:
+            # that was the last build, so we can notify finished watchers. If
+            # we haven't failed by now, we can claim success.
+            if self.stillHopeful:
+                self.status.setResults(builder.SUCCESS)
+                self.status.notifySuccessWatchers()
+            self.status.notifyFinishedWatchers()
+
new file mode 100644
--- /dev/null
+++ b/buildbot/buildslave.py
@@ -0,0 +1,350 @@
+
+import time
+from email.Message import Message
+from email.Utils import formatdate
+from zope.interface import implements
+from twisted.python import log
+from twisted.internet import defer, reactor
+from twisted.application import service
+
+from buildbot.pbutil import NewCredPerspective
+from buildbot.status.builder import SlaveStatus
+from buildbot.status.mail import MailNotifier
+from buildbot.interfaces import IBuildSlave
+from buildbot.process.properties import Properties
+
+class BuildSlave(NewCredPerspective, service.MultiService):
+    """This is the master-side representative for a remote buildbot slave.
+    There is exactly one for each slave described in the config file (the
+    c['slaves'] list). When buildbots connect in (.attach), they get a
+    reference to this instance. The BotMaster object is stashed as the
+    .botmaster attribute. The BotMaster is also our '.parent' Service.
+
+    I represent a build slave -- a remote machine capable of
+    running builds.  I am instantiated by the configuration file, and can be
+    subclassed to add extra functionality."""
+
+    implements(IBuildSlave)
+
+    def __init__(self, name, password, max_builds=None,
+                 notify_on_missing=[], missing_timeout=3600,
+                 properties={}):
+        """
+        @param name: botname this machine will supply when it connects
+        @param password: password this machine will supply when
+                         it connects
+        @param max_builds: maximum number of simultaneous builds that will
+                           be run concurrently on this buildslave (the
+                           default is None for no limit)
+        @param properties: properties that will be applied to builds run on 
+                           this slave
+        @type properties: dictionary
+        """
+        service.MultiService.__init__(self)
+        self.slavename = name
+        self.password = password
+        self.botmaster = None # no buildmaster yet
+        self.slave_status = SlaveStatus(name)
+        self.slave = None # a RemoteReference to the Bot, when connected
+        self.slave_commands = None
+        self.slavebuilders = []
+        self.max_builds = max_builds
+
+        self.properties = Properties()
+        self.properties.update(properties, "BuildSlave")
+        self.properties.setProperty("slavename", name, "BuildSlave")
+
+        self.lastMessageReceived = 0
+        if isinstance(notify_on_missing, str):
+            notify_on_missing = [notify_on_missing]
+        self.notify_on_missing = notify_on_missing
+        for i in notify_on_missing:
+            assert isinstance(i, str)
+        self.missing_timeout = missing_timeout
+        self.missing_timer = None
+
+    def update(self, new):
+        """
+        Given a new BuildSlave, configure this one identically.  Because
+        BuildSlave objects are remotely referenced, we can't replace them
+        without disconnecting the slave, yet there's no reason to do that.
+        """
+        # the reconfiguration logic should guarantee this:
+        assert self.slavename == new.slavename
+        assert self.password == new.password
+        assert self.__class__ == new.__class__
+        self.max_builds = new.max_builds
+
+    def __repr__(self):
+        if self.botmaster:
+            builders = self.botmaster.getBuildersForSlave(self.slavename)
+            return "<BuildSlave '%s', current builders: %s>" % \
+               (self.slavename, ','.join(map(lambda b: b.name, builders)))
+        else:
+            return "<BuildSlave '%s', (no builders yet)>" % self.slavename
+
+    def setBotmaster(self, botmaster):
+        assert not self.botmaster, "BuildSlave already has a botmaster"
+        self.botmaster = botmaster
+
+    def updateSlave(self):
+        """Called to add or remove builders after the slave has connected.
+
+        @return: a Deferred that indicates when an attached slave has
+        accepted the new builders and/or released the old ones."""
+        if self.slave:
+            return self.sendBuilderList()
+        return defer.succeed(None)
+
+    def updateSlaveStatus(self, buildStarted=None, buildFinished=None):
+        if buildStarted:
+            self.slave_status.buildStarted(buildStarted)
+        if buildFinished:
+            self.slave_status.buildFinished(buildFinished)
+
+    def attached(self, bot):
+        """This is called when the slave connects.
+
+        @return: a Deferred that fires with a suitable pb.IPerspective to
+                 give to the slave (i.e. 'self')"""
+
+        if self.slave:
+            # uh-oh, we've got a duplicate slave. The most likely
+            # explanation is that the slave is behind a slow link, thinks we
+            # went away, and has attempted to reconnect, so we've got two
+            # "connections" from the same slave, but the previous one is
+            # stale. Give the new one precedence.
+            log.msg("duplicate slave %s replacing old one" % self.slavename)
+
+            # just in case we've got two identically-configured slaves,
+            # report the IP addresses of both so someone can resolve the
+            # squabble
+            tport = self.slave.broker.transport
+            log.msg("old slave was connected from", tport.getPeer())
+            log.msg("new slave is from", bot.broker.transport.getPeer())
+            d = self.disconnect()
+        else:
+            d = defer.succeed(None)
+        # now we go through a sequence of calls, gathering information, then
+        # tell the Botmaster that it can finally give this slave to all the
+        # Builders that care about it.
+
+        # we accumulate slave information in this 'state' dictionary, then
+        # set it atomically if we make it far enough through the process
+        state = {}
+
+        def _log_attachment_on_slave(res):
+            d1 = bot.callRemote("print", "attached")
+            d1.addErrback(lambda why: None)
+            return d1
+        d.addCallback(_log_attachment_on_slave)
+
+        def _get_info(res):
+            d1 = bot.callRemote("getSlaveInfo")
+            def _got_info(info):
+                log.msg("Got slaveinfo from '%s'" % self.slavename)
+                # TODO: info{} might have other keys
+                state["admin"] = info.get("admin")
+                state["host"] = info.get("host")
+            def _info_unavailable(why):
+                # maybe an old slave, doesn't implement remote_getSlaveInfo
+                log.msg("BuildSlave.info_unavailable")
+                log.err(why)
+            d1.addCallbacks(_got_info, _info_unavailable)
+            return d1
+        d.addCallback(_get_info)
+
+        def _get_commands(res):
+            d1 = bot.callRemote("getCommands")
+            def _got_commands(commands):
+                state["slave_commands"] = commands
+            def _commands_unavailable(why):
+                # probably an old slave
+                log.msg("BuildSlave._commands_unavailable")
+                if why.check(AttributeError):
+                    return
+                log.err(why)
+            d1.addCallbacks(_got_commands, _commands_unavailable)
+            return d1
+        d.addCallback(_get_commands)
+
+        def _accept_slave(res):
+            self.slave_status.setAdmin(state.get("admin"))
+            self.slave_status.setHost(state.get("host"))
+            self.slave_status.setConnected(True)
+            self.slave_commands = state.get("slave_commands")
+            self.slave = bot
+            log.msg("bot attached")
+            self.messageReceivedFromSlave()
+            if self.missing_timer:
+                self.missing_timer.cancel()
+                self.missing_timer = None
+
+            return self.updateSlave()
+        d.addCallback(_accept_slave)
+
+        # Finally, the slave gets a reference to this BuildSlave. They
+        # receive this later, after we've started using them.
+        d.addCallback(lambda res: self)
+        return d
+
+    def messageReceivedFromSlave(self):
+        now = time.time()
+        self.lastMessageReceived = now
+        self.slave_status.setLastMessageReceived(now)
+
+    def detached(self, mind):
+        self.slave = None
+        self.slave_status.setConnected(False)
+        self.botmaster.slaveLost(self)
+        log.msg("BuildSlave.detached(%s)" % self.slavename)
+        if self.notify_on_missing and self.parent and not self.missing_timer:
+            self.missing_timer = reactor.callLater(self.missing_timeout,
+                                                   self._missing_timer_fired)
+
+    def _missing_timer_fired(self):
+        self.missing_timer = None
+        # notify people, but only if we're still in the config
+        if not self.parent:
+            return
+
+        # first, see if we have a MailNotifier we can use. This gives us a
+        # fromaddr and a relayhost.
+        buildmaster = self.botmaster.parent
+        status = buildmaster.getStatus()
+        for st in buildmaster.statusTargets:
+            if isinstance(st, MailNotifier):
+                break
+        else:
+            # if not, they get a default MailNotifier, which always uses SMTP
+            # to localhost and uses a dummy fromaddr of "buildbot".
+            log.msg("buildslave-missing msg using default MailNotifier")
+            st = MailNotifier("buildbot")
+        # now construct the mail
+        text = "The Buildbot working for '%s'\n" % status.getProjectName()
+        text += ("has noticed that the buildslave named %s went away\n" %
+                 self.slavename)
+        text += "\n"
+        text += ("It last disconnected at %s (buildmaster-local time)\n" %
+                 time.ctime(time.time() - self.missing_timeout)) # close enough
+        text += "\n"
+        text += "The admin on record (as reported by BUILDSLAVE:info/admin)\n"
+        text += "was '%s'.\n" % self.slave_status.getAdmin()
+        text += "\n"
+        text += "Sincerely,\n"
+        text += " The Buildbot\n"
+        text += " %s\n" % status.getProjectURL()
+
+        m = Message()
+        m.set_payload(text)
+        m['Date'] = formatdate(localtime=True)
+        m['Subject'] = "Buildbot: buildslave %s was lost" % self.slavename
+        m['From'] = st.fromaddr
+        recipients = self.notify_on_missing
+        m['To'] = ", ".join(recipients)
+        d = st.sendMessage(m, recipients)
+        # return the Deferred for testing purposes
+        return d
+
+    def disconnect(self):
+        """Forcibly disconnect the slave.
+
+        This severs the TCP connection and returns a Deferred that will fire
+        (with None) when the connection is probably gone.
+
+        If the slave is still alive, they will probably try to reconnect
+        again in a moment.
+
+        This is called in two circumstances. The first is when a slave is
+        removed from the config file. In this case, when they try to
+        reconnect, they will be rejected as an unknown slave. The second is
+        when we wind up with two connections for the same slave, in which
+        case we disconnect the older connection.
+        """
+
+        if not self.slave:
+            return defer.succeed(None)
+        log.msg("disconnecting old slave %s now" % self.slavename)
+
+        # all kinds of teardown will happen as a result of
+        # loseConnection(), but it happens after a reactor iteration or
+        # two. Hook the actual disconnect so we can know when it is safe
+        # to connect the new slave. We have to wait one additional
+        # iteration (with callLater(0)) to make sure the *other*
+        # notifyOnDisconnect handlers have had a chance to run.
+        d = defer.Deferred()
+
+        # notifyOnDisconnect runs the callback with one argument, the
+        # RemoteReference being disconnected.
+        def _disconnected(rref):
+            reactor.callLater(0, d.callback, None)
+        self.slave.notifyOnDisconnect(_disconnected)
+        tport = self.slave.broker.transport
+        # this is the polite way to request that a socket be closed
+        tport.loseConnection()
+        try:
+            # but really we don't want to wait for the transmit queue to
+            # drain. The remote end is unlikely to ACK the data, so we'd
+            # probably have to wait for a (20-minute) TCP timeout.
+            #tport._closeSocket()
+            # however, doing _closeSocket (whether before or after
+            # loseConnection) somehow prevents the notifyOnDisconnect
+            # handlers from being run. Bummer.
+            tport.offset = 0
+            tport.dataBuffer = ""
+            pass
+        except:
+            # however, these hacks are pretty internal, so don't blow up if
+            # they fail or are unavailable
+            log.msg("failed to accelerate the shutdown process")
+            pass
+        log.msg("waiting for slave to finish disconnecting")
+
+        # When this Deferred fires, we'll be ready to accept the new slave
+        return d
+
+    def sendBuilderList(self):
+        our_builders = self.botmaster.getBuildersForSlave(self.slavename)
+        blist = [(b.name, b.builddir) for b in our_builders]
+        d = self.slave.callRemote("setBuilderList", blist)
+        def _sent(slist):
+            dl = []
+            for name, remote in slist.items():
+                # use get() since we might have changed our mind since then
+                b = self.botmaster.builders.get(name)
+                if b:
+                    d1 = b.attached(self, remote, self.slave_commands)
+                    dl.append(d1)
+            return defer.DeferredList(dl)
+        def _set_failed(why):
+            log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
+            log.err(why)
+            # TODO: hang up on them?, without setBuilderList we can't use
+            # them
+        d.addCallbacks(_sent, _set_failed)
+        return d
+
+    def perspective_keepalive(self):
+        pass
+
+    def addSlaveBuilder(self, sb):
+        log.msg("%s adding %s" % (self, sb))
+        self.slavebuilders.append(sb)
+
+    def removeSlaveBuilder(self, sb):
+        log.msg("%s removing %s" % (self, sb))
+        if sb in self.slavebuilders:
+            self.slavebuilders.remove(sb)
+
+    def canStartBuild(self):
+        """
+        I am called when a build is requested to see if this buildslave
+        can start a build.  This function can be used to limit overall
+        concurrency on the buildslave.
+        """
+        if self.max_builds:
+            active_builders = [sb for sb in self.slavebuilders if sb.isBusy()]
+            if len(active_builders) >= self.max_builds:
+                return False
+        return True
+
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/base.py
@@ -0,0 +1,10 @@
+
+from zope.interface import implements
+from twisted.application import service
+
+from buildbot.interfaces import IChangeSource
+from buildbot import util
+
+class ChangeSource(service.Service, util.ComparableMixin):
+    implements(IChangeSource)
+
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/bonsaipoller.py
@@ -0,0 +1,320 @@
+import time
+from xml.dom import minidom
+
+from twisted.python import log, failure
+from twisted.internet import reactor
+from twisted.internet.task import LoopingCall
+from twisted.web.client import getPage
+
+from buildbot.changes import base, changes
+
+class InvalidResultError(Exception):
+    def __init__(self, value="InvalidResultError"):
+        self.value = value
+    def __str__(self):
+        return repr(self.value)
+
+class EmptyResult(Exception):
+    pass
+
+class NoMoreCiNodes(Exception):
+    pass
+
+class NoMoreFileNodes(Exception):
+    pass
+
+class BonsaiResult:
+    """I hold a list of CiNodes"""
+    def __init__(self, nodes=[]):
+        self.nodes = nodes
+
+    def __cmp__(self, other):
+        if len(self.nodes) != len(other.nodes):
+            return False
+        for i in range(len(self.nodes)):
+            if self.nodes[i].log != other.nodes[i].log \
+              or self.nodes[i].who != other.nodes[i].who \
+              or self.nodes[i].date != other.nodes[i].date \
+              or len(self.nodes[i].files) != len(other.nodes[i].files):
+                return -1
+
+	        for j in range(len(self.nodes[i].files)):
+	            if self.nodes[i].files[j].revision \
+	              != other.nodes[i].files[j].revision \
+	              or self.nodes[i].files[j].filename \
+	              != other.nodes[i].files[j].filename:
+	                return -1
+
+        return 0
+
+class CiNode:
+    """I hold information baout one <ci> node, including a list of files"""
+    def __init__(self, log="", who="", date=0, files=[]):
+        self.log = log
+        self.who = who
+        self.date = date
+        self.files = files
+
+class FileNode:
+    """I hold information about one <f> node"""
+    def __init__(self, revision="", filename=""):
+        self.revision = revision
+        self.filename = filename
+
+class BonsaiParser:
+    """I parse the XML result from a bonsai cvsquery."""
+
+    def __init__(self, data):
+        try:
+        # this is a fix for non-ascii characters
+        # because bonsai does not give us an encoding to work with
+        # it impossible to be 100% sure what to decode it as but latin1 covers
+        # the broadest base
+            data = data.decode("latin1")
+            data = data.encode("ascii", "replace")
+            self.dom = minidom.parseString(data)
+            log.msg(data)
+        except:
+            raise InvalidResultError("Malformed XML in result")
+
+        self.ciNodes = self.dom.getElementsByTagName("ci")
+        self.currentCiNode = None # filled in by _nextCiNode()
+        self.fileNodes = None # filled in by _nextCiNode()
+        self.currentFileNode = None # filled in by _nextFileNode()
+        self.bonsaiResult = self._parseData()
+
+    def getData(self):
+        return self.bonsaiResult
+
+    def _parseData(self):
+        """Returns data from a Bonsai cvsquery in a BonsaiResult object"""
+        nodes = []
+        try:
+            while self._nextCiNode():
+                files = []
+                try:
+                    while self._nextFileNode():
+                        files.append(FileNode(self._getRevision(),
+                                              self._getFilename()))
+                except NoMoreFileNodes:
+                    pass
+                except InvalidResultError:
+                    raise
+                cinode = CiNode(self._getLog(), self._getWho(),
+                                self._getDate(), files)
+                # hack around bonsai xml output bug for empty check-in comments
+                if not cinode.log and nodes and \
+                        not nodes[-1].log and \
+                        cinode.who == nodes[-1].who and \
+                        cinode.date == nodes[-1].date:
+                    nodes[-1].files += cinode.files
+                else:
+                    nodes.append(cinode)
+
+        except NoMoreCiNodes:
+            pass
+        except InvalidResultError, EmptyResult:
+            raise
+
+        return BonsaiResult(nodes)
+
+
+    def _nextCiNode(self):
+        """Iterates to the next <ci> node and fills self.fileNodes with
+           child <f> nodes"""
+        try:
+            self.currentCiNode = self.ciNodes.pop(0)
+            if len(self.currentCiNode.getElementsByTagName("files")) > 1:
+                raise InvalidResultError("Multiple <files> for one <ci>")
+
+            self.fileNodes = self.currentCiNode.getElementsByTagName("f")
+        except IndexError:
+            # if there was zero <ci> nodes in the result
+            if not self.currentCiNode:
+                raise EmptyResult
+            else:
+                raise NoMoreCiNodes
+
+        return True
+
+    def _nextFileNode(self):
+        """Iterates to the next <f> node"""
+        try:
+            self.currentFileNode = self.fileNodes.pop(0)
+        except IndexError:
+            raise NoMoreFileNodes
+
+        return True
+
+    def _getLog(self):
+        """Returns the log of the current <ci> node"""
+        logs = self.currentCiNode.getElementsByTagName("log")
+        if len(logs) < 1:
+            raise InvalidResultError("No log present")
+        elif len(logs) > 1:
+            raise InvalidResultError("Multiple logs present")
+
+        # catch empty check-in comments
+        if logs[0].firstChild:
+            return logs[0].firstChild.data
+        return ''
+
+    def _getWho(self):
+        """Returns the e-mail address of the commiter"""
+        # convert unicode string to regular string
+        return str(self.currentCiNode.getAttribute("who"))
+
+    def _getDate(self):
+        """Returns the date (unix time) of the commit"""
+        # convert unicode number to regular one
+        try:
+            commitDate = int(self.currentCiNode.getAttribute("date"))
+        except ValueError:
+            raise InvalidResultError
+
+        return commitDate
+
+    def _getFilename(self):
+        """Returns the filename of the current <f> node"""
+        try:
+            filename = self.currentFileNode.firstChild.data
+        except AttributeError:
+            raise InvalidResultError("Missing filename")
+
+        return filename
+
+    def _getRevision(self):
+        return self.currentFileNode.getAttribute("rev")
+
+
+class BonsaiPoller(base.ChangeSource):
+    """This source will poll a bonsai server for changes and submit
+    them to the change master."""
+
+    compare_attrs = ["bonsaiURL", "pollInterval", "tree",
+                     "module", "branch", "cvsroot"]
+
+    parent = None # filled in when we're added
+    loop = None
+    volatile = ['loop']
+    working = False
+
+    def __init__(self, bonsaiURL, module, branch, tree="default",
+                 cvsroot="/cvsroot", pollInterval=30):
+        """
+        @type   bonsaiURL:      string
+        @param  bonsaiURL:      The base URL of the Bonsai server
+                                (ie. http://bonsai.mozilla.org)
+        @type   module:         string
+        @param  module:         The module to look for changes in. Commonly
+                                this is 'all'
+        @type   branch:         string
+        @param  branch:         The branch to look for changes in. This must
+                                match the
+                                'branch' option for the Scheduler.
+        @type   tree:           string
+        @param  tree:           The tree to look for changes in. Commonly this
+                                is 'all'
+        @type   cvsroot:        string
+        @param  cvsroot:        The cvsroot of the repository. Usually this is
+                                '/cvsroot'
+        @type   pollInterval:   int
+        @param  pollInterval:   The time (in seconds) between queries for
+                                changes
+        """
+
+        self.bonsaiURL = bonsaiURL
+        self.module = module
+        self.branch = branch
+        self.tree = tree
+        self.cvsroot = cvsroot
+        self.pollInterval = pollInterval
+        self.lastChange = time.time()
+        self.lastPoll = time.time()
+
+    def startService(self):
+        self.loop = LoopingCall(self.poll)
+        base.ChangeSource.startService(self)
+
+        reactor.callLater(0, self.loop.start, self.pollInterval)
+
+    def stopService(self):
+        self.loop.stop()
+        return base.ChangeSource.stopService(self)
+
+    def describe(self):
+        str = ""
+        str += "Getting changes from the Bonsai service running at %s " \
+                % self.bonsaiURL
+        str += "<br>Using tree: %s, branch: %s, and module: %s" % (self.tree, \
+                self.branch, self.module)
+        return str
+
+    def poll(self):
+        if self.working:
+            log.msg("Not polling Bonsai because last poll is still working")
+        else:
+            self.working = True
+            d = self._get_changes()
+            d.addCallback(self._process_changes)
+            d.addCallbacks(self._finished_ok, self._finished_failure)
+        return
+
+    def _finished_ok(self, res):
+        assert self.working
+        self.working = False
+
+        # check for failure -- this is probably never hit but the twisted docs
+        # are not clear enough to be sure. it is being kept "just in case"
+        if isinstance(res, failure.Failure):
+            log.msg("Bonsai poll failed: %s" % res)
+        return res
+
+    def _finished_failure(self, res):
+        log.msg("Bonsai poll failed: %s" % res)
+        assert self.working
+        self.working = False
+        return None # eat the failure
+
+    def _make_url(self):
+        args = ["treeid=%s" % self.tree, "module=%s" % self.module,
+                "branch=%s" % self.branch, "branchtype=match",
+                "sortby=Date", "date=explicit",
+                "mindate=%d" % self.lastChange,
+                "maxdate=%d" % int(time.time()),
+                "cvsroot=%s" % self.cvsroot, "xml=1"]
+        # build the bonsai URL
+        url = self.bonsaiURL
+        url += "/cvsquery.cgi?"
+        url += "&".join(args)
+
+        return url
+
+    def _get_changes(self):
+        url = self._make_url()
+        log.msg("Polling Bonsai tree at %s" % url)
+
+        self.lastPoll = time.time()
+        # get the page, in XML format
+        return getPage(url, timeout=self.pollInterval)
+
+    def _process_changes(self, query):
+        try:
+            bp = BonsaiParser(query)
+            result = bp.getData()
+        except InvalidResultError, e:
+            log.msg("Could not process Bonsai query: " + e.value)
+            return
+        except EmptyResult:
+            return
+
+        for cinode in result.nodes:
+            files = [file.filename + ' (revision '+file.revision+')'
+                     for file in cinode.files]
+            c = changes.Change(who = cinode.who,
+                               files = files,
+                               comments = cinode.log,
+                               when = cinode.date,
+                               branch = self.branch)
+            self.parent.addChange(c)
+            self.lastChange = self.lastPoll
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/changes.py
@@ -0,0 +1,277 @@
+
+import sys, os, time
+from cPickle import dump
+
+from zope.interface import implements
+from twisted.python import log
+from twisted.internet import defer
+from twisted.application import service
+from twisted.web import html
+
+from buildbot import interfaces, util
+
+html_tmpl = """
+<p>Changed by: <b>%(who)s</b><br />
+Changed at: <b>%(at)s</b><br />
+%(branch)s
+%(revision)s
+<br />
+
+Changed files:
+%(files)s
+
+Comments:
+%(comments)s
+</p>
+"""
+
+class Change:
+    """I represent a single change to the source tree. This may involve
+    several files, but they are all changed by the same person, and there is
+    a change comment for the group as a whole.
+
+    If the version control system supports sequential repository- (or
+    branch-) wide change numbers (like SVN, P4, and Arch), then revision=
+    should be set to that number. The highest such number will be used at
+    checkout time to get the correct set of files.
+
+    If it does not (like CVS), when= should be set to the timestamp (seconds
+    since epoch, as returned by time.time()) when the change was made. when=
+    will be filled in for you (to the current time) if you omit it, which is
+    suitable for ChangeSources which have no way of getting more accurate
+    timestamps.
+
+    Changes should be submitted to ChangeMaster.addChange() in
+    chronologically increasing order. Out-of-order changes will probably
+    cause the html.Waterfall display to be corrupted."""
+
+    implements(interfaces.IStatusEvent)
+
+    number = None
+
+    links = []
+    branch = None
+    revision = None # used to create a source-stamp
+
+    def __init__(self, who, files, comments, isdir=0, links=[],
+                 revision=None, when=None, branch=None):
+        self.who = who
+        self.files = files
+        self.comments = comments
+        self.isdir = isdir
+        self.links = links
+        self.revision = revision
+        if when is None:
+            when = util.now()
+        self.when = when
+        self.branch = branch
+
+    def asText(self):
+        data = ""
+        data += self.getFileContents() 
+        data += "At: %s\n" % self.getTime()
+        data += "Changed By: %s\n" % self.who
+        data += "Comments: %s\n\n" % self.comments
+        return data
+
+    def asHTML(self):
+        links = []
+        for file in self.files:
+            link = filter(lambda s: s.find(file) != -1, self.links)
+            if len(link) == 1:
+                # could get confused
+                links.append('<a href="%s"><b>%s</b></a>' % (link[0], file))
+            else:
+                links.append('<b>%s</b>' % file)
+        revision = ""
+        if self.revision:
+            revision = "Revision: <b>%s</b><br />\n" % self.revision
+        branch = ""
+        if self.branch:
+            branch = "Branch: <b>%s</b><br />\n" % self.branch
+
+        kwargs = { 'who'     : html.escape(self.who),
+                   'at'      : self.getTime(),
+                   'files'   : html.UL(links) + '\n',
+                   'revision': revision,
+                   'branch'  : branch,
+                   'comments': html.PRE(self.comments) }
+        return html_tmpl % kwargs
+
+    def get_HTML_box(self, url):
+        """Return the contents of a TD cell for the waterfall display.
+
+        @param url: the URL that points to an HTML page that will render
+        using our asHTML method. The Change is free to use this or ignore it
+        as it pleases.
+
+        @return: the HTML that will be put inside the table cell. Typically
+        this is just a single href named after the author of the change and
+        pointing at the passed-in 'url'.
+        """
+        who = self.getShortAuthor()
+        return '<a href="%s" title="%s">%s</a>' % (url,
+                                                   html.escape(self.comments),
+                                                   html.escape(who))
+
+    def getShortAuthor(self):
+        return self.who
+
+    def getTime(self):
+        if not self.when:
+            return "?"
+        return time.strftime("%a %d %b %Y %H:%M:%S",
+                             time.localtime(self.when))
+
+    def getTimes(self):
+        return (self.when, None)
+
+    def getText(self):
+        return [html.escape(self.who)]
+    def getColor(self):
+        return "white"
+    def getLogs(self):
+        return {}
+
+    def getFileContents(self):
+        data = ""
+        if len(self.files) == 1:
+            if self.isdir:
+                data += "Directory: %s\n" % self.files[0]
+            else:
+                data += "File: %s\n" % self.files[0]
+        else:
+            data += "Files:\n"
+            for f in self.files:
+                data += " %s\n" % f
+        return data
+        
+class ChangeMaster(service.MultiService):
+
+    """This is the master-side service which receives file change
+    notifications from CVS. It keeps a log of these changes, enough to
+    provide for the HTML waterfall display, and to tell
+    temporarily-disconnected bots what they missed while they were
+    offline.
+
+    Change notifications come from two different kinds of sources. The first
+    is a PB service (servicename='changemaster', perspectivename='change'),
+    which provides a remote method called 'addChange', which should be
+    called with a dict that has keys 'filename' and 'comments'.
+
+    The second is a list of objects derived from the ChangeSource class.
+    These are added with .addSource(), which also sets the .changemaster
+    attribute in the source to point at the ChangeMaster. When the
+    application begins, these will be started with .start() . At shutdown
+    time, they will be terminated with .stop() . They must be persistable.
+    They are expected to call self.changemaster.addChange() with Change
+    objects.
+
+    There are several different variants of the second type of source:
+    
+      - L{buildbot.changes.mail.MaildirSource} watches a maildir for CVS
+        commit mail. It uses DNotify if available, or polls every 10
+        seconds if not.  It parses incoming mail to determine what files
+        were changed.
+
+      - L{buildbot.changes.freshcvs.FreshCVSSource} makes a PB
+        connection to the CVSToys 'freshcvs' daemon and relays any
+        changes it announces.
+    
+    """
+
+    implements(interfaces.IEventSource)
+
+    debug = False
+    # todo: use Maildir class to watch for changes arriving by mail
+
+    def __init__(self):
+        service.MultiService.__init__(self)
+        self.changes = []
+        # self.basedir must be filled in by the parent
+        self.nextNumber = 1
+
+    def addSource(self, source):
+        assert interfaces.IChangeSource.providedBy(source)
+        assert service.IService.providedBy(source)
+        if self.debug:
+            print "ChangeMaster.addSource", source
+        source.setServiceParent(self)
+
+    def removeSource(self, source):
+        assert source in self
+        if self.debug:
+            print "ChangeMaster.removeSource", source, source.parent
+        d = defer.maybeDeferred(source.disownServiceParent)
+        return d
+
+    def addChange(self, change):
+        """Deliver a file change event. The event should be a Change object.
+        This method will timestamp the object as it is received."""
+        log.msg("adding change, who %s, %d files, rev=%s, branch=%s, "
+                "comments %s" % (change.who, len(change.files),
+                                 change.revision, change.branch,
+                                 change.comments))
+        change.number = self.nextNumber
+        self.nextNumber += 1
+        self.changes.append(change)
+        self.parent.addChange(change)
+        # TODO: call pruneChanges after a while
+
+    def pruneChanges(self):
+        self.changes = self.changes[-100:] # or something
+
+    def eventGenerator(self, branches=[]):
+        for i in range(len(self.changes)-1, -1, -1):
+            c = self.changes[i]
+            if not branches or c.branch in branches:
+                yield c
+
+    def getChangeNumbered(self, num):
+        if not self.changes:
+            return None
+        first = self.changes[0].number
+        if first + len(self.changes)-1 != self.changes[-1].number:
+            log.msg(self,
+                    "lost a change somewhere: [0] is %d, [%d] is %d" % \
+                    (self.changes[0].number,
+                     len(self.changes) - 1,
+                     self.changes[-1].number))
+            for c in self.changes:
+                log.msg("c[%d]: " % c.number, c)
+            return None
+        offset = num - first
+        log.msg(self, "offset", offset)
+        return self.changes[offset]
+
+    def __getstate__(self):
+        d = service.MultiService.__getstate__(self)
+        del d['parent']
+        del d['services'] # lose all children
+        del d['namedServices']
+        return d
+
+    def __setstate__(self, d):
+        self.__dict__ = d
+        # self.basedir must be set by the parent
+        self.services = [] # they'll be repopulated by readConfig
+        self.namedServices = {}
+
+
+    def saveYourself(self):
+        filename = os.path.join(self.basedir, "changes.pck")
+        tmpfilename = filename + ".tmp"
+        try:
+            dump(self, open(tmpfilename, "wb"))
+            if sys.platform == 'win32':
+                # windows cannot rename a file on top of an existing one
+                if os.path.exists(filename):
+                    os.unlink(filename)
+            os.rename(tmpfilename, filename)
+        except Exception, e:
+            log.msg("unable to save changes")
+            log.err()
+
+    def stopService(self):
+        self.saveYourself()
+        return service.MultiService.stopService(self)
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/dnotify.py
@@ -0,0 +1,100 @@
+
+import fcntl, signal, os
+
+class DNotify_Handler:
+    def __init__(self):
+        self.watchers = {}
+        self.installed = 0
+    def install(self):
+        if self.installed:
+            return
+        signal.signal(signal.SIGIO, self.fire)
+        self.installed = 1
+    def uninstall(self):
+        if not self.installed:
+            return
+        signal.signal(signal.SIGIO, signal.SIG_DFL)
+        self.installed = 0
+    def add(self, watcher):
+        self.watchers[watcher.fd] = watcher
+        self.install()
+    def remove(self, watcher):
+        if self.watchers.has_key(watcher.fd):
+            del(self.watchers[watcher.fd])
+            if not self.watchers:
+                self.uninstall()
+    def fire(self, signum, frame):
+        # this is the signal handler
+        # without siginfo_t, we must fire them all
+        for watcher in self.watchers.values():
+            watcher.callback()
+            
+class DNotify:
+    DN_ACCESS = fcntl.DN_ACCESS  # a file in the directory was read
+    DN_MODIFY = fcntl.DN_MODIFY  # a file was modified (write,truncate)
+    DN_CREATE = fcntl.DN_CREATE  # a file was created
+    DN_DELETE = fcntl.DN_DELETE  # a file was unlinked
+    DN_RENAME = fcntl.DN_RENAME  # a file was renamed
+    DN_ATTRIB = fcntl.DN_ATTRIB  # a file had attributes changed (chmod,chown)
+
+    handler = [None]
+    
+    def __init__(self, dirname, callback=None,
+                 flags=[DN_MODIFY,DN_CREATE,DN_DELETE,DN_RENAME]):
+
+        """This object watches a directory for changes. The .callback
+        attribute should be set to a function to be run every time something
+        happens to it. Be aware that it will be called more times than you
+        expect."""
+
+        if callback:
+            self.callback = callback
+        else:
+            self.callback = self.fire
+        self.dirname = dirname
+        self.flags = reduce(lambda x, y: x | y, flags) | fcntl.DN_MULTISHOT
+        self.fd = os.open(dirname, os.O_RDONLY)
+        # ideally we would move the notification to something like SIGRTMIN,
+        # (to free up SIGIO) and use sigaction to have the signal handler
+        # receive a structure with the fd number. But python doesn't offer
+        # either.
+        if not self.handler[0]:
+            self.handler[0] = DNotify_Handler()
+        self.handler[0].add(self)
+        fcntl.fcntl(self.fd, fcntl.F_NOTIFY, self.flags)
+    def remove(self):
+        self.handler[0].remove(self)
+        os.close(self.fd)
+    def fire(self):
+        print self.dirname, "changed!"
+
+def test_dnotify1():
+    d = DNotify(".")
+    while 1:
+        signal.pause()
+
+def test_dnotify2():
+    # create ./foo/, create/delete files in ./ and ./foo/ while this is
+    # running. Notice how both notifiers are fired when anything changes;
+    # this is an unfortunate side-effect of the lack of extended sigaction
+    # support in Python.
+    count = [0]
+    d1 = DNotify(".")
+    def fire1(count=count, d1=d1):
+        print "./ changed!", count[0]
+        count[0] += 1
+        if count[0] > 5:
+            d1.remove()
+            del(d1)
+    # change the callback, since we can't define it until after we have the
+    # dnotify object. Hmm, unless we give the dnotify to the callback.
+    d1.callback = fire1
+    def fire2(): print "foo/ changed!"
+    d2 = DNotify("foo", fire2)
+    while 1:
+        signal.pause()
+        
+    
+if __name__ == '__main__':
+    test_dnotify2()
+    
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/freshcvs.py
@@ -0,0 +1,144 @@
+
+import os.path
+
+from zope.interface import implements
+from twisted.cred import credentials
+from twisted.spread import pb
+from twisted.application.internet import TCPClient
+from twisted.python import log
+
+import cvstoys.common # to make sure VersionedPatch gets registered
+
+from buildbot.interfaces import IChangeSource
+from buildbot.pbutil import ReconnectingPBClientFactory
+from buildbot.changes.changes import Change
+from buildbot import util
+
+class FreshCVSListener(pb.Referenceable):
+    def remote_notify(self, root, files, message, user):
+        try:
+            self.source.notify(root, files, message, user)
+        except Exception, e:
+            print "notify failed"
+            log.err()
+
+    def remote_goodbye(self, message):
+        pass
+
+class FreshCVSConnectionFactory(ReconnectingPBClientFactory):
+
+    def gotPerspective(self, perspective):
+        log.msg("connected to FreshCVS daemon")
+        ReconnectingPBClientFactory.gotPerspective(self, perspective)
+        self.source.connected = True
+        # TODO: freshcvs-1.0.10 doesn't handle setFilter correctly, it will
+        # be fixed in the upcoming 1.0.11 . I haven't been able to test it
+        # to make sure the failure mode is survivable, so I'll just leave
+        # this out for now.
+        return
+        if self.source.prefix is not None:
+            pathfilter = "^%s" % self.source.prefix
+            d = perspective.callRemote("setFilter",
+                                       None, pathfilter, None)
+            # ignore failures, setFilter didn't work in 1.0.10 and this is
+            # just an optimization anyway
+            d.addErrback(lambda f: None)
+
+    def clientConnectionLost(self, connector, reason):
+        ReconnectingPBClientFactory.clientConnectionLost(self, connector,
+                                                         reason)
+        self.source.connected = False
+
+class FreshCVSSourceNewcred(TCPClient, util.ComparableMixin):
+    """This source will connect to a FreshCVS server associated with one or
+    more CVS repositories. Each time a change is committed to a repository,
+    the server will send us a message describing the change. This message is
+    used to build a Change object, which is then submitted to the
+    ChangeMaster.
+
+    This class handles freshcvs daemons which use newcred. CVSToys-1.0.9
+    does not, later versions might.
+    """
+
+    implements(IChangeSource)
+    compare_attrs = ["host", "port", "username", "password", "prefix"]
+
+    changemaster = None # filled in when we're added
+    connected = False
+
+    def __init__(self, host, port, user, passwd, prefix=None):
+        self.host = host
+        self.port = port
+        self.username = user
+        self.password = passwd
+        if prefix is not None and not prefix.endswith("/"):
+            log.msg("WARNING: prefix '%s' should probably end with a slash" \
+                    % prefix)
+        self.prefix = prefix
+        self.listener = l = FreshCVSListener()
+        l.source = self
+        self.factory = f = FreshCVSConnectionFactory()
+        f.source = self
+        self.creds = credentials.UsernamePassword(user, passwd)
+        f.startLogin(self.creds, client=l)
+        TCPClient.__init__(self, host, port, f)
+
+    def __repr__(self):
+        return "<FreshCVSSource where=%s, prefix=%s>" % \
+               ((self.host, self.port), self.prefix)
+
+    def describe(self):
+        online = ""
+        if not self.connected:
+            online = " [OFFLINE]"
+        return "freshcvs %s:%s%s" % (self.host, self.port, online)
+
+    def notify(self, root, files, message, user):
+        pathnames = []
+        isdir = 0
+        for f in files:
+            if not isinstance(f, (cvstoys.common.VersionedPatch,
+                                  cvstoys.common.Directory)):
+                continue
+            pathname, filename = f.pathname, f.filename
+            #r1, r2 = getattr(f, 'r1', None), getattr(f, 'r2', None)
+            if isinstance(f, cvstoys.common.Directory):
+                isdir = 1
+            path = os.path.join(pathname, filename)
+            log.msg("FreshCVS notify '%s'" % path)
+            if self.prefix:
+                if path.startswith(self.prefix):
+                    path = path[len(self.prefix):]
+                else:
+                    continue
+            pathnames.append(path)
+        if pathnames:
+            # now() is close enough: FreshCVS *is* realtime, after all
+            when=util.now()
+            c = Change(user, pathnames, message, isdir, when=when)
+            self.parent.addChange(c)
+
+class FreshCVSSourceOldcred(FreshCVSSourceNewcred):
+    """This is for older freshcvs daemons (from CVSToys-1.0.9 and earlier).
+    """
+
+    def __init__(self, host, port, user, passwd,
+                 serviceName="cvstoys.notify", prefix=None):
+        self.host = host
+        self.port = port
+        self.prefix = prefix
+        self.listener = l = FreshCVSListener()
+        l.source = self
+        self.factory = f = FreshCVSConnectionFactory()
+        f.source = self
+        f.startGettingPerspective(user, passwd, serviceName, client=l)
+        TCPClient.__init__(self, host, port, f)
+
+    def __repr__(self):
+        return "<FreshCVSSourceOldcred where=%s, prefix=%s>" % \
+               ((self.host, self.port), self.prefix)
+
+# this is suitable for CVSToys-1.0.10 and later. If you run CVSToys-1.0.9 or
+# earlier, use FreshCVSSourceOldcred instead.
+FreshCVSSource = FreshCVSSourceNewcred
+
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/hgbuildbot.py
@@ -0,0 +1,107 @@
+# hgbuildbot.py - mercurial hooks for buildbot
+#
+# Copyright 2007 Frederic Leroy <fredo@starox.org>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+# hook extension to send change notifications to buildbot when a changeset is
+# brought into the repository from elsewhere.
+#
+# default mode is to use mercurial branch
+#
+# to use, configure hgbuildbot in .hg/hgrc like this:
+#
+#   [hooks]
+#   changegroup = python:buildbot.changes.hgbuildbot.hook
+#
+#   [hgbuildbot]
+#   # config items go in here
+#
+# config items:
+#
+# REQUIRED:
+#   master = host:port                   # host to send buildbot changes
+#
+# OPTIONAL:
+#   branchtype = inrepo|dirname          # dirname: branch = name of directory
+#                                        #          containing the repository
+#                                        #
+#                                        # inrepo:  branch = mercurial branch
+#
+#   branch = branchname                  # if set, branch is always branchname
+
+import os
+
+from mercurial.i18n import gettext as _
+from mercurial.node import bin, hex
+
+# mercurial's on-demand-importing hacks interfere with the:
+#from zope.interface import Interface
+# that Twisted needs to do, so disable it.
+try:
+    from mercurial import demandimport
+    demandimport.disable()
+except ImportError:
+    pass
+
+from buildbot.clients import sendchange
+from twisted.internet import defer, reactor
+
+
+def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
+    # read config parameters
+    master = ui.config('hgbuildbot', 'master')
+    if master:
+        branchtype = ui.config('hgbuildbot', 'branchtype')
+        branch = ui.config('hgbuildbot', 'branch')
+    else:
+        ui.write("* You must add a [hgbuildbot] section to .hg/hgrc in "
+                 "order to use buildbot hook\n")
+        return
+
+    if branch is None:
+        if branchtype is not None:
+            if branchtype == 'dirname':
+                branch = os.path.basename(os.getcwd())
+            if branchtype == 'inrepo':
+                branch=repo.workingctx().branch()
+
+    if hooktype == 'changegroup':
+        s = sendchange.Sender(master, None)
+        d = defer.Deferred()
+        reactor.callLater(0, d.callback, None)
+        # process changesets
+        def _send(res, c):
+            ui.status("rev %s sent\n" % c['revision'])
+            return s.send(c['branch'], c['revision'], c['comments'],
+                          c['files'], c['username'])
+
+        node=bin(node)
+        start = repo.changelog.rev(node)
+        end = repo.changelog.count()
+        for rev in xrange(start, end):
+            # send changeset
+            n = repo.changelog.node(rev)
+            changeset=repo.changelog.read(n)
+            change = {
+                'master': master,
+                # note: this is more likely to be a full email address, which
+                # would make the left-hand "Changes" column kind of wide. The
+                # buildmaster should probably be improved to display an
+                # abbreviation of the username.
+                'username': changeset[1],
+                'revision': hex(n),
+                'comments': changeset[4],
+                'files': changeset[3],
+                'branch': branch
+            }
+            d.addCallback(_send, change)
+
+        d.addCallbacks(s.printSuccess, s.printFailure)
+        d.addBoth(s.stop)
+        s.run()
+    else:
+        ui.status(_('hgbuildbot: hook %s not supported\n') % hooktype)
+    return
+
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/mail.py
@@ -0,0 +1,458 @@
+# -*- test-case-name: buildbot.test.test_mailparse -*-
+
+"""
+Parse various kinds of 'CVS notify' email.
+"""
+import os, re
+from email import message_from_file
+from email.Utils import parseaddr
+from email.Iterators import body_line_iterator
+
+from zope.interface import implements
+from twisted.python import log
+from buildbot import util
+from buildbot.interfaces import IChangeSource
+from buildbot.changes import changes
+from buildbot.changes.maildir import MaildirService
+
+class MaildirSource(MaildirService, util.ComparableMixin):
+    """This source will watch a maildir that is subscribed to a FreshCVS
+    change-announcement mailing list.
+    """
+    implements(IChangeSource)
+
+    compare_attrs = ["basedir", "pollinterval"]
+    name = None
+
+    def __init__(self, maildir, prefix=None):
+        MaildirService.__init__(self, maildir)
+        self.prefix = prefix
+        if prefix and not prefix.endswith("/"):
+            log.msg("%s: you probably want your prefix=('%s') to end with "
+                    "a slash")
+
+    def describe(self):
+        return "%s mailing list in maildir %s" % (self.name, self.basedir)
+
+    def messageReceived(self, filename):
+        path = os.path.join(self.basedir, "new", filename)
+        change = self.parse_file(open(path, "r"), self.prefix)
+        if change:
+            self.parent.addChange(change)
+        os.rename(os.path.join(self.basedir, "new", filename),
+                  os.path.join(self.basedir, "cur", filename))
+
+    def parse_file(self, fd, prefix=None):
+        m = message_from_file(fd)
+        return self.parse(m, prefix)
+
+class FCMaildirSource(MaildirSource):
+    name = "FreshCVS"
+
+    def parse(self, m, prefix=None):
+        """Parse mail sent by FreshCVS"""
+
+        # FreshCVS sets From: to "user CVS <user>", but the <> part may be
+        # modified by the MTA (to include a local domain)
+        name, addr = parseaddr(m["from"])
+        if not name:
+            return None # no From means this message isn't from FreshCVS
+        cvs = name.find(" CVS")
+        if cvs == -1:
+            return None # this message isn't from FreshCVS
+        who = name[:cvs]
+
+        # we take the time of receipt as the time of checkin. Not correct,
+        # but it avoids the out-of-order-changes issue. See the comment in
+        # parseSyncmail about using the 'Date:' header
+        when = util.now()
+
+        files = []
+        comments = ""
+        isdir = 0
+        lines = list(body_line_iterator(m))
+        while lines:
+            line = lines.pop(0)
+            if line == "Modified files:\n":
+                break
+        while lines:
+            line = lines.pop(0)
+            if line == "\n":
+                break
+            line = line.rstrip("\n")
+            linebits = line.split(None, 1)
+            file = linebits[0]
+            if prefix:
+                # insist that the file start with the prefix: FreshCVS sends
+                # changes we don't care about too
+                if file.startswith(prefix):
+                    file = file[len(prefix):]
+                else:
+                    continue
+            if len(linebits) == 1:
+                isdir = 1
+            elif linebits[1] == "0 0":
+                isdir = 1
+            files.append(file)
+        while lines:
+            line = lines.pop(0)
+            if line == "Log message:\n":
+                break
+        # message is terminated by "ViewCVS links:" or "Index:..." (patch)
+        while lines:
+            line = lines.pop(0)
+            if line == "ViewCVS links:\n":
+                break
+            if line.find("Index: ") == 0:
+                break
+            comments += line
+        comments = comments.rstrip() + "\n"
+
+        if not files:
+            return None
+
+        change = changes.Change(who, files, comments, isdir, when=when)
+
+        return change
+
+class SyncmailMaildirSource(MaildirSource):
+    name = "Syncmail"
+
+    def parse(self, m, prefix=None):
+        """Parse messages sent by the 'syncmail' program, as suggested by the
+        sourceforge.net CVS Admin documentation. Syncmail is maintained at
+        syncmail.sf.net .
+        """
+        # pretty much the same as freshcvs mail, not surprising since CVS is
+        # the one creating most of the text
+
+        # The mail is sent from the person doing the checkin. Assume that the
+        # local username is enough to identify them (this assumes a one-server
+        # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS
+        # model)
+        name, addr = parseaddr(m["from"])
+        if not addr:
+            return None # no From means this message isn't from FreshCVS
+        at = addr.find("@")
+        if at == -1:
+            who = addr # might still be useful
+        else:
+            who = addr[:at]
+
+        # we take the time of receipt as the time of checkin. Not correct (it
+        # depends upon the email latency), but it avoids the
+        # out-of-order-changes issue. Also syncmail doesn't give us anything
+        # better to work with, unless you count pulling the v1-vs-v2
+        # timestamp out of the diffs, which would be ugly. TODO: Pulling the
+        # 'Date:' header from the mail is a possibility, and
+        # email.Utils.parsedate_tz may be useful. It should be configurable,
+        # however, because there are a lot of broken clocks out there.
+        when = util.now()
+
+        subject = m["subject"]
+        # syncmail puts the repository-relative directory in the subject:
+        # mprefix + "%(dir)s %(file)s,%(oldversion)s,%(newversion)s", where
+        # 'mprefix' is something that could be added by a mailing list
+        # manager.
+        # this is the only reasonable way to determine the directory name
+        space = subject.find(" ")
+        if space != -1:
+            directory = subject[:space]
+        else:
+            directory = subject
+
+        files = []
+        comments = ""
+        isdir = 0
+        branch = None
+
+        lines = list(body_line_iterator(m))
+        while lines:
+            line = lines.pop(0)
+
+            if (line == "Modified Files:\n" or
+                line == "Added Files:\n" or
+                line == "Removed Files:\n"):
+                break
+
+        while lines:
+            line = lines.pop(0)
+            if line == "\n":
+                break
+            if line == "Log Message:\n":
+                lines.insert(0, line)
+                break
+            line = line.lstrip()
+            line = line.rstrip()
+            # note: syncmail will send one email per directory involved in a
+            # commit, with multiple files if they were in the same directory.
+            # Unlike freshCVS, it makes no attempt to collect all related
+            # commits into a single message.
+
+            # note: syncmail will report a Tag underneath the ... Files: line
+            # e.g.:       Tag: BRANCH-DEVEL
+
+            if line.startswith('Tag:'):
+                branch = line.split(' ')[-1].rstrip()
+                continue
+
+            thesefiles = line.split(" ")
+            for f in thesefiles:
+                f = directory + "/" + f
+                if prefix:
+                    # insist that the file start with the prefix: we may get
+                    # changes we don't care about too
+                    if f.startswith(prefix):
+                        f = f[len(prefix):]
+                    else:
+                        continue
+                        break
+                # TODO: figure out how new directories are described, set
+                # .isdir
+                files.append(f)
+
+        if not files:
+            return None
+
+        while lines:
+            line = lines.pop(0)
+            if line == "Log Message:\n":
+                break
+        # message is terminated by "Index:..." (patch) or "--- NEW FILE.."
+        # or "--- filename DELETED ---". Sigh.
+        while lines:
+            line = lines.pop(0)
+            if line.find("Index: ") == 0:
+                break
+            if re.search(r"^--- NEW FILE", line):
+                break
+            if re.search(r" DELETED ---$", line):
+                break
+            comments += line
+        comments = comments.rstrip() + "\n"
+
+        change = changes.Change(who, files, comments, isdir, when=when,
+                                branch=branch)
+
+        return change
+
+# Bonsai mail parser by Stephen Davis.
+#
+# This handles changes for CVS repositories that are watched by Bonsai
+# (http://www.mozilla.org/bonsai.html)
+
+# A Bonsai-formatted email message looks like:
+# 
+# C|1071099907|stephend|/cvs|Sources/Scripts/buildbot|bonsai.py|1.2|||18|7
+# A|1071099907|stephend|/cvs|Sources/Scripts/buildbot|master.cfg|1.1|||18|7
+# R|1071099907|stephend|/cvs|Sources/Scripts/buildbot|BuildMaster.py|||
+# LOGCOMMENT
+# Updated bonsai parser and switched master config to buildbot-0.4.1 style.
+# 
+# :ENDLOGCOMMENT
+#
+# In the first example line, stephend is the user, /cvs the repository,
+# buildbot the directory, bonsai.py the file, 1.2 the revision, no sticky
+# and branch, 18 lines added and 7 removed. All of these fields might not be
+# present (during "removes" for example).
+#
+# There may be multiple "control" lines or even none (imports, directory
+# additions) but there is one email per directory. We only care about actual
+# changes since it is presumed directory additions don't actually affect the
+# build. At least one file should need to change (the makefile, say) to
+# actually make a new directory part of the build process. That's my story
+# and I'm sticking to it.
+
+class BonsaiMaildirSource(MaildirSource):
+    name = "Bonsai"
+
+    def parse(self, m, prefix=None):
+        """Parse mail sent by the Bonsai cvs loginfo script."""
+
+        # we don't care who the email came from b/c the cvs user is in the
+        # msg text
+
+        who = "unknown"
+        timestamp = None
+        files = []
+        lines = list(body_line_iterator(m))
+
+        # read the control lines (what/who/where/file/etc.)
+        while lines:
+            line = lines.pop(0)
+            if line == "LOGCOMMENT\n":
+                break;
+            line = line.rstrip("\n")
+
+            # we'd like to do the following but it won't work if the number of
+            # items doesn't match so...
+            #   what, timestamp, user, repo, module, file = line.split( '|' )
+            items = line.split('|')
+            if len(items) < 6:
+                # not a valid line, assume this isn't a bonsai message
+                return None
+
+            try:
+                # just grab the bottom-most timestamp, they're probably all the
+                # same. TODO: I'm assuming this is relative to the epoch, but
+                # this needs testing.
+                timestamp = int(items[1])
+            except ValueError:
+                pass
+
+            user = items[2]
+            if user:
+                who = user
+
+            module = items[4]
+            file = items[5]
+            if module and file:
+                path = "%s/%s" % (module, file)
+                files.append(path)
+            sticky = items[7]
+            branch = items[8]
+
+        # if no files changed, return nothing
+        if not files:
+            return None
+
+        # read the comments
+        comments = ""
+        while lines:
+            line = lines.pop(0)
+            if line == ":ENDLOGCOMMENT\n":
+                break
+            comments += line
+        comments = comments.rstrip() + "\n"
+
+        # return buildbot Change object
+        return changes.Change(who, files, comments, when=timestamp,
+                              branch=branch)
+
+# svn "commit-email.pl" handler.  The format is very similar to freshcvs mail;
+# here's a sample:
+
+#  From: username [at] apache.org    [slightly obfuscated to avoid spam here]
+#  To: commits [at] spamassassin.apache.org
+#  Subject: svn commit: r105955 - in spamassassin/trunk: . lib/Mail
+#  ...
+#
+#  Author: username
+#  Date: Sat Nov 20 00:17:49 2004      [note: TZ = local tz on server!]
+#  New Revision: 105955
+#
+#  Modified:   [also Removed: and Added:]
+#    [filename]
+#    ...
+#  Log:
+#  [log message]
+#  ...
+#
+#
+#  Modified: spamassassin/trunk/lib/Mail/SpamAssassin.pm
+#  [unified diff]
+#
+#  [end of mail]
+
+class SVNCommitEmailMaildirSource(MaildirSource):
+    name = "SVN commit-email.pl"
+
+    def parse(self, m, prefix=None):
+        """Parse messages sent by the svn 'commit-email.pl' trigger.
+        """
+
+        # The mail is sent from the person doing the checkin. Assume that the
+        # local username is enough to identify them (this assumes a one-server
+        # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS
+        # model)
+        name, addr = parseaddr(m["from"])
+        if not addr:
+            return None # no From means this message isn't from FreshCVS
+        at = addr.find("@")
+        if at == -1:
+            who = addr # might still be useful
+        else:
+            who = addr[:at]
+
+        # we take the time of receipt as the time of checkin. Not correct (it
+        # depends upon the email latency), but it avoids the
+        # out-of-order-changes issue. Also syncmail doesn't give us anything
+        # better to work with, unless you count pulling the v1-vs-v2
+        # timestamp out of the diffs, which would be ugly. TODO: Pulling the
+        # 'Date:' header from the mail is a possibility, and
+        # email.Utils.parsedate_tz may be useful. It should be configurable,
+        # however, because there are a lot of broken clocks out there.
+        when = util.now()
+
+        files = []
+        comments = ""
+        isdir = 0
+        lines = list(body_line_iterator(m))
+        rev = None
+        while lines:
+            line = lines.pop(0)
+
+            # "Author: jmason"
+            match = re.search(r"^Author: (\S+)", line)
+            if match:
+                who = match.group(1)
+
+            # "New Revision: 105955"
+            match = re.search(r"^New Revision: (\d+)", line)
+            if match:
+                rev = match.group(1)
+
+            # possible TODO: use "Date: ..." data here instead of time of
+            # commit message receipt, above. however, this timestamp is
+            # specified *without* a timezone, in the server's local TZ, so to
+            # be accurate buildbot would need a config setting to specify the
+            # source server's expected TZ setting! messy.
+
+            # this stanza ends with the "Log:"
+            if (line == "Log:\n"):
+                break
+
+        # commit message is terminated by the file-listing section
+        while lines:
+            line = lines.pop(0)
+            if (line == "Modified:\n" or
+                line == "Added:\n" or
+                line == "Removed:\n"):
+                break
+            comments += line
+        comments = comments.rstrip() + "\n"
+
+        while lines:
+            line = lines.pop(0)
+            if line == "\n":
+                break
+            if line.find("Modified:\n") == 0:
+                continue            # ignore this line
+            if line.find("Added:\n") == 0:
+                continue            # ignore this line
+            if line.find("Removed:\n") == 0:
+                continue            # ignore this line
+            line = line.strip()
+
+            thesefiles = line.split(" ")
+            for f in thesefiles:
+                if prefix:
+                    # insist that the file start with the prefix: we may get
+                    # changes we don't care about too
+                    if f.startswith(prefix):
+                        f = f[len(prefix):]
+                    else:
+                        log.msg("ignored file from svn commit: prefix '%s' "
+                                "does not match filename '%s'" % (prefix, f))
+                        continue
+
+                # TODO: figure out how new directories are described, set
+                # .isdir
+                files.append(f)
+
+        if not files:
+            log.msg("no matching files found, ignoring commit")
+            return None
+
+        return changes.Change(who, files, comments, when=when, revision=rev)
+
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/maildir.py
@@ -0,0 +1,116 @@
+
+# This is a class which watches a maildir for new messages. It uses the
+# linux dirwatcher API (if available) to look for new files. The
+# .messageReceived method is invoked with the filename of the new message,
+# relative to the top of the maildir (so it will look like "new/blahblah").
+
+import os
+from twisted.python import log
+from twisted.application import service, internet
+from twisted.internet import reactor
+dnotify = None
+try:
+    import dnotify
+except:
+    # I'm not actually sure this log message gets recorded
+    log.msg("unable to import dnotify, so Maildir will use polling instead")
+
+class NoSuchMaildir(Exception):
+    pass
+
+class MaildirService(service.MultiService):
+    """I watch a maildir for new messages. I should be placed as the service
+    child of some MultiService instance. When running, I use the linux
+    dirwatcher API (if available) or poll for new files in the 'new'
+    subdirectory of my maildir path. When I discover a new message, I invoke
+    my .messageReceived() method with the short filename of the new message,
+    so the full name of the new file can be obtained with
+    os.path.join(maildir, 'new', filename). messageReceived() should be
+    overridden by a subclass to do something useful. I will not move or
+    delete the file on my own: the subclass's messageReceived() should
+    probably do that.
+    """
+    pollinterval = 10  # only used if we don't have DNotify
+
+    def __init__(self, basedir=None):
+        """Create the Maildir watcher. BASEDIR is the maildir directory (the
+        one which contains new/ and tmp/)
+        """
+        service.MultiService.__init__(self)
+        self.basedir = basedir
+        self.files = []
+        self.dnotify = None
+
+    def setBasedir(self, basedir):
+        # some users of MaildirService (scheduler.Try_Jobdir, in particular)
+        # don't know their basedir until setServiceParent, since it is
+        # relative to the buildmaster's basedir. So let them set it late. We
+        # don't actually need it until our own startService.
+        self.basedir = basedir
+
+    def startService(self):
+        service.MultiService.startService(self)
+        self.newdir = os.path.join(self.basedir, "new")
+        if not os.path.isdir(self.basedir) or not os.path.isdir(self.newdir):
+            raise NoSuchMaildir("invalid maildir '%s'" % self.basedir)
+        try:
+            if dnotify:
+                # we must hold an fd open on the directory, so we can get
+                # notified when it changes.
+                self.dnotify = dnotify.DNotify(self.newdir,
+                                               self.dnotify_callback,
+                                               [dnotify.DNotify.DN_CREATE])
+        except (IOError, OverflowError):
+            # IOError is probably linux<2.4.19, which doesn't support
+            # dnotify. OverflowError will occur on some 64-bit machines
+            # because of a python bug
+            log.msg("DNotify failed, falling back to polling")
+        if not self.dnotify:
+            t = internet.TimerService(self.pollinterval, self.poll)
+            t.setServiceParent(self)
+        self.poll()
+
+    def dnotify_callback(self):
+        log.msg("dnotify noticed something, now polling")
+
+        # give it a moment. I found that qmail had problems when the message
+        # was removed from the maildir instantly. It shouldn't, that's what
+        # maildirs are made for. I wasn't able to eyeball any reason for the
+        # problem, and safecat didn't behave the same way, but qmail reports
+        # "Temporary_error_on_maildir_delivery" (qmail-local.c:165,
+        # maildir_child() process exited with rc not in 0,2,3,4). Not sure
+        # why, and I'd have to hack qmail to investigate further, so it's
+        # easier to just wait a second before yanking the message out of new/
+
+        reactor.callLater(0.1, self.poll)
+
+
+    def stopService(self):
+        if self.dnotify:
+            self.dnotify.remove()
+            self.dnotify = None
+        return service.MultiService.stopService(self)
+
+    def poll(self):
+        assert self.basedir
+        # see what's new
+        for f in self.files:
+            if not os.path.isfile(os.path.join(self.newdir, f)):
+                self.files.remove(f)
+        newfiles = []
+        for f in os.listdir(self.newdir):
+            if not f in self.files:
+                newfiles.append(f)
+        self.files.extend(newfiles)
+        # TODO: sort by ctime, then filename, since safecat uses a rather
+        # fine-grained timestamp in the filename
+        for n in newfiles:
+            # TODO: consider catching exceptions in messageReceived
+            self.messageReceived(n)
+
+    def messageReceived(self, filename):
+        """Called when a new file is noticed. Will call
+        self.parent.messageReceived() with a path relative to maildir/new.
+        Should probably be overridden in subclasses."""
+        self.parent.messageReceived(filename)
+
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/monotone.py
@@ -0,0 +1,305 @@
+
+import tempfile
+import os
+from cStringIO import StringIO
+
+from twisted.python import log
+from twisted.application import service
+from twisted.internet import defer, protocol, error, reactor
+from twisted.internet.task import LoopingCall
+
+from buildbot import util
+from buildbot.interfaces import IChangeSource
+from buildbot.changes.changes import Change
+
+class _MTProtocol(protocol.ProcessProtocol):
+
+    def __init__(self, deferred, cmdline):
+        self.cmdline = cmdline
+        self.deferred = deferred
+        self.s = StringIO()
+
+    def errReceived(self, text):
+        log.msg("stderr: %s" % text)
+
+    def outReceived(self, text):
+        log.msg("stdout: %s" % text)
+        self.s.write(text)
+
+    def processEnded(self, reason):
+        log.msg("Command %r exited with value %s" % (self.cmdline, reason))
+        if isinstance(reason.value, error.ProcessDone):
+            self.deferred.callback(self.s.getvalue())
+        else:
+            self.deferred.errback(reason)
+
+class Monotone:
+    """All methods of this class return a Deferred."""
+
+    def __init__(self, bin, db):
+        self.bin = bin
+        self.db = db
+
+    def _run_monotone(self, args):
+        d = defer.Deferred()
+        cmdline = (self.bin, "--db=" + self.db) + tuple(args)
+        p = _MTProtocol(d, cmdline)
+        log.msg("Running command: %r" % (cmdline,))
+        log.msg("wd: %s" % os.getcwd())
+        reactor.spawnProcess(p, self.bin, cmdline)
+        return d
+
+    def _process_revision_list(self, output):
+        if output:
+            return output.strip().split("\n")
+        else:
+            return []
+
+    def get_interface_version(self):
+        d = self._run_monotone(["automate", "interface_version"])
+        d.addCallback(self._process_interface_version)
+        return d
+
+    def _process_interface_version(self, output):
+        return tuple(map(int, output.strip().split(".")))
+
+    def db_init(self):
+        return self._run_monotone(["db", "init"])
+
+    def db_migrate(self):
+        return self._run_monotone(["db", "migrate"])
+
+    def pull(self, server, pattern):
+        return self._run_monotone(["pull", server, pattern])
+
+    def get_revision(self, rid):
+        return self._run_monotone(["cat", "revision", rid])
+
+    def get_heads(self, branch, rcfile=""):
+        cmd = ["automate", "heads", branch]
+        if rcfile:
+            cmd += ["--rcfile=" + rcfile]
+        d = self._run_monotone(cmd)
+        d.addCallback(self._process_revision_list)
+        return d
+
+    def erase_ancestors(self, revs):
+        d = self._run_monotone(["automate", "erase_ancestors"] + revs)
+        d.addCallback(self._process_revision_list)
+        return d
+
+    def ancestry_difference(self, new_rev, old_revs):
+        d = self._run_monotone(["automate", "ancestry_difference", new_rev]
+                               + old_revs)
+        d.addCallback(self._process_revision_list)
+        return d
+
+    def descendents(self, rev):
+        d = self._run_monotone(["automate", "descendents", rev])
+        d.addCallback(self._process_revision_list)
+        return d
+
+    def log(self, rev, depth=None):
+        if depth is not None:
+            depth_arg = ["--last=%i" % (depth,)]
+        else:
+            depth_arg = []
+        return self._run_monotone(["log", "-r", rev] + depth_arg)
+
+
+class MonotoneSource(service.Service, util.ComparableMixin):
+    """This source will poll a monotone server for changes and submit them to
+    the change master.
+
+    @param server_addr: monotone server specification (host:portno)
+
+    @param branch: monotone branch to watch
+
+    @param trusted_keys: list of keys whose code you trust
+
+    @param db_path: path to monotone database to pull into
+
+    @param pollinterval: interval in seconds between polls, defaults to 10 minutes
+    @param monotone_exec: path to monotone executable, defaults to "monotone"
+    """
+
+    __implements__ = IChangeSource, service.Service.__implements__
+    compare_attrs = ["server_addr", "trusted_keys", "db_path",
+                     "pollinterval", "branch", "monotone_exec"]
+
+    parent = None # filled in when we're added
+    done_revisions = []
+    last_revision = None
+    loop = None
+    d = None
+    tmpfile = None
+    monotone = None
+    volatile = ["loop", "d", "tmpfile", "monotone"]
+
+    def __init__(self, server_addr, branch, trusted_keys, db_path,
+                 pollinterval=60 * 10, monotone_exec="monotone"):
+        self.server_addr = server_addr
+        self.branch = branch
+        self.trusted_keys = trusted_keys
+        self.db_path = db_path
+        self.pollinterval = pollinterval
+        self.monotone_exec = monotone_exec
+        self.monotone = Monotone(self.monotone_exec, self.db_path)
+
+    def startService(self):
+        self.loop = LoopingCall(self.start_poll)
+        self.loop.start(self.pollinterval)
+        service.Service.startService(self)
+
+    def stopService(self):
+        self.loop.stop()
+        return service.Service.stopService(self)
+
+    def describe(self):
+        return "monotone_source %s %s" % (self.server_addr,
+                                          self.branch)
+
+    def start_poll(self):
+        if self.d is not None:
+            log.msg("last poll still in progress, skipping next poll")
+            return
+        log.msg("starting poll")
+        self.d = self._maybe_init_db()
+        self.d.addCallback(self._do_netsync)
+        self.d.addCallback(self._get_changes)
+        self.d.addErrback(self._handle_error)
+
+    def _handle_error(self, failure):
+        log.err(failure)
+        self.d = None
+
+    def _maybe_init_db(self):
+        if not os.path.exists(self.db_path):
+            log.msg("init'ing db")
+            return self.monotone.db_init()
+        else:
+            log.msg("db already exists, migrating")
+            return self.monotone.db_migrate()
+
+    def _do_netsync(self, output):
+        return self.monotone.pull(self.server_addr, self.branch)
+
+    def _get_changes(self, output):
+        d = self._get_new_head()
+        d.addCallback(self._process_new_head)
+        return d
+
+    def _get_new_head(self):
+        # This function returns a deferred that resolves to a good pick of new
+        # head (or None if there is no good new head.)
+
+        # First need to get all new heads...
+        rcfile = """function get_revision_cert_trust(signers, id, name, val)
+                      local trusted_signers = { %s }
+                      local ts_table = {}
+                      for k, v in pairs(trusted_signers) do ts_table[v] = 1 end
+                      for k, v in pairs(signers) do
+                        if ts_table[v] then
+                          return true
+                        end
+                      end
+                      return false
+                    end
+        """
+        trusted_list = ", ".join(['"' + key + '"' for key in self.trusted_keys])
+        # mktemp is unsafe, but mkstemp is not 2.2 compatible.
+        tmpfile_name = tempfile.mktemp()
+        f = open(tmpfile_name, "w")
+        f.write(rcfile % trusted_list)
+        f.close()
+        d = self.monotone.get_heads(self.branch, tmpfile_name)
+        d.addCallback(self._find_new_head, tmpfile_name)
+        return d
+
+    def _find_new_head(self, new_heads, tmpfile_name):
+        os.unlink(tmpfile_name)
+        # Now get the old head's descendents...
+        if self.last_revision is not None:
+            d = self.monotone.descendents(self.last_revision)
+        else:
+            d = defer.succeed(new_heads)
+        d.addCallback(self._pick_new_head, new_heads)
+        return d
+
+    def _pick_new_head(self, old_head_descendents, new_heads):
+        for r in new_heads:
+            if r in old_head_descendents:
+                return r
+        return None
+
+    def _process_new_head(self, new_head):
+        if new_head is None:
+            log.msg("No new head")
+            self.d = None
+            return None
+        # Okay, we have a new head; we need to get all the revisions since
+        # then and create change objects for them.
+        # Step 1: simplify set of processed revisions.
+        d = self._simplify_revisions()
+        # Step 2: get the list of new revisions
+        d.addCallback(self._get_new_revisions, new_head)
+        # Step 3: add a change for each
+        d.addCallback(self._add_changes_for_revisions)
+        # Step 4: all done
+        d.addCallback(self._finish_changes, new_head)
+        return d
+
+    def _simplify_revisions(self):
+        d = self.monotone.erase_ancestors(self.done_revisions)
+        d.addCallback(self._reset_done_revisions)
+        return d
+
+    def _reset_done_revisions(self, new_done_revisions):
+        self.done_revisions = new_done_revisions
+        return None
+
+    def _get_new_revisions(self, blah, new_head):
+        if self.done_revisions:
+            return self.monotone.ancestry_difference(new_head,
+                                                     self.done_revisions)
+        else:
+            # Don't force feed the builder with every change since the
+            # beginning of time when it's first started up.
+            return defer.succeed([new_head])
+
+    def _add_changes_for_revisions(self, revs):
+        d = defer.succeed(None)
+        for rid in revs:
+            d.addCallback(self._add_change_for_revision, rid)
+        return d
+
+    def _add_change_for_revision(self, blah, rid):
+        d = self.monotone.log(rid, 1)
+        d.addCallback(self._add_change_from_log, rid)
+        return d
+
+    def _add_change_from_log(self, log, rid):
+        d = self.monotone.get_revision(rid)
+        d.addCallback(self._add_change_from_log_and_revision, log, rid)
+        return d
+
+    def _add_change_from_log_and_revision(self, revision, log, rid):
+        # Stupid way to pull out everything inside quotes (which currently
+        # uniquely identifies filenames inside a changeset).
+        pieces = revision.split('"')
+        files = []
+        for i in range(len(pieces)):
+            if (i % 2) == 1:
+                files.append(pieces[i])
+        # Also pull out author key and date
+        author = "unknown author"
+        pieces = log.split('\n')
+        for p in pieces:
+            if p.startswith("Author:"):
+                author = p.split()[1]
+        self.parent.addChange(Change(author, files, log, revision=rid))
+
+    def _finish_changes(self, blah, new_head):
+        self.done_revisions.append(new_head)
+        self.last_revision = new_head
+        self.d = None
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/p4poller.py
@@ -0,0 +1,207 @@
+# -*- test-case-name: buildbot.test.test_p4poller -*-
+
+# Many thanks to Dave Peticolas for contributing this module
+
+import re
+import time
+
+from twisted.python import log, failure
+from twisted.internet import defer, reactor
+from twisted.internet.utils import getProcessOutput
+from twisted.internet.task import LoopingCall
+
+from buildbot import util
+from buildbot.changes import base, changes
+
+def get_simple_split(branchfile):
+    """Splits the branchfile argument and assuming branch is 
+       the first path component in branchfile, will return
+       branch and file else None."""
+
+    index = branchfile.find('/')
+    if index == -1: return None, None
+    branch, file = branchfile.split('/', 1)
+    return branch, file
+
+class P4Source(base.ChangeSource, util.ComparableMixin):
+    """This source will poll a perforce repository for changes and submit
+    them to the change master."""
+
+    compare_attrs = ["p4port", "p4user", "p4passwd", "p4base",
+                     "p4bin", "pollinterval"]
+
+    changes_line_re = re.compile(
+            r"Change (?P<num>\d+) on \S+ by \S+@\S+ '.+'$")
+    describe_header_re = re.compile(
+            r"Change \d+ by (?P<who>\S+)@\S+ on (?P<when>.+)$")
+    file_re = re.compile(r"^\.\.\. (?P<path>[^#]+)#\d+ \w+$")
+    datefmt = '%Y/%m/%d %H:%M:%S'
+
+    parent = None # filled in when we're added
+    last_change = None
+    loop = None
+    working = False
+
+    def __init__(self, p4port=None, p4user=None, p4passwd=None,
+                 p4base='//', p4bin='p4',
+                 split_file=lambda branchfile: (None, branchfile),
+                 pollinterval=60 * 10, histmax=None):
+        """
+        @type  p4port:       string
+        @param p4port:       p4 port definition (host:portno)
+        @type  p4user:       string
+        @param p4user:       p4 user
+        @type  p4passwd:     string
+        @param p4passwd:     p4 passwd
+        @type  p4base:       string
+        @param p4base:       p4 file specification to limit a poll to
+                             without the trailing '...' (i.e., //)
+        @type  p4bin:        string
+        @param p4bin:        path to p4 binary, defaults to just 'p4'
+        @type  split_file:   func
+        $param split_file:   splits a filename into branch and filename.
+        @type  pollinterval: int
+        @param pollinterval: interval in seconds between polls
+        @type  histmax:      int
+        @param histmax:      (obsolete) maximum number of changes to look back through.
+                             ignored; accepted for backwards compatibility.
+        """
+
+        self.p4port = p4port
+        self.p4user = p4user
+        self.p4passwd = p4passwd
+        self.p4base = p4base
+        self.p4bin = p4bin
+        self.split_file = split_file
+        self.pollinterval = pollinterval
+        self.loop = LoopingCall(self.checkp4)
+
+    def startService(self):
+        base.ChangeSource.startService(self)
+
+        # Don't start the loop just yet because the reactor isn't running.
+        # Give it a chance to go and install our SIGCHLD handler before
+        # spawning processes.
+        reactor.callLater(0, self.loop.start, self.pollinterval)
+
+    def stopService(self):
+        self.loop.stop()
+        return base.ChangeSource.stopService(self)
+
+    def describe(self):
+        return "p4source %s %s" % (self.p4port, self.p4base)
+
+    def checkp4(self):
+        # Our return value is only used for unit testing.
+        if self.working:
+            log.msg("Skipping checkp4 because last one has not finished")
+            return defer.succeed(None)
+        else:
+            self.working = True
+            d = self._get_changes()
+            d.addCallback(self._process_changes)
+            d.addBoth(self._finished)
+            return d
+
+    def _finished(self, res):
+        assert self.working
+        self.working = False
+
+        # Again, the return value is only for unit testing.
+        # If there's a failure, log it so it isn't lost.
+        if isinstance(res, failure.Failure):
+            log.msg('P4 poll failed: %s' % res)
+            return None
+        return res
+
+    def _get_changes(self):
+        args = []
+        if self.p4port:
+            args.extend(['-p', self.p4port])
+        if self.p4user:
+            args.extend(['-u', self.p4user])
+        if self.p4passwd:
+            args.extend(['-P', self.p4passwd])
+        args.extend(['changes'])
+        if self.last_change is not None:
+            args.extend(['%s...@%d,now' % (self.p4base, self.last_change+1)])
+        else:
+            args.extend(['-m', '1', '%s...' % (self.p4base,)])
+        env = {}
+        return getProcessOutput(self.p4bin, args, env)
+
+    def _process_changes(self, result):
+        last_change = self.last_change
+        changelists = []
+        for line in result.split('\n'):
+            line = line.strip()
+            if not line: continue
+            m = self.changes_line_re.match(line)
+            assert m, "Unexpected 'p4 changes' output: %r" % result
+            num = int(m.group('num'))
+            if last_change is None:
+                log.msg('P4Poller: starting at change %d' % num)
+                self.last_change = num
+                return []
+            changelists.append(num)
+        changelists.reverse() # oldest first
+
+        # Retrieve each sequentially.
+        d = defer.succeed(None)
+        for c in changelists:
+            d.addCallback(self._get_describe, c)
+            d.addCallback(self._process_describe, c)
+        return d
+
+    def _get_describe(self, dummy, num):
+        args = []
+        if self.p4port:
+            args.extend(['-p', self.p4port])
+        if self.p4user:
+            args.extend(['-u', self.p4user])
+        if self.p4passwd:
+            args.extend(['-P', self.p4passwd])
+        args.extend(['describe', '-s', str(num)])
+        env = {}
+        d = getProcessOutput(self.p4bin, args, env)
+        return d
+
+    def _process_describe(self, result, num):
+        lines = result.split('\n')
+        # SF#1555985: Wade Brainerd reports a stray ^M at the end of the date
+        # field. The rstrip() is intended to remove that.
+        lines[0] = lines[0].rstrip()
+        m = self.describe_header_re.match(lines[0])
+        assert m, "Unexpected 'p4 describe -s' result: %r" % result
+        who = m.group('who')
+        when = time.mktime(time.strptime(m.group('when'), self.datefmt))
+        comments = ''
+        while not lines[0].startswith('Affected files'):
+            comments += lines.pop(0) + '\n'
+        lines.pop(0) # affected files
+
+        branch_files = {} # dict for branch mapped to file(s)
+        while lines:
+            line = lines.pop(0).strip()
+            if not line: continue
+            m = self.file_re.match(line)
+            assert m, "Invalid file line: %r" % line
+            path = m.group('path')
+            if path.startswith(self.p4base):
+                branch, file = self.split_file(path[len(self.p4base):])
+                if (branch == None and file == None): continue
+                if branch_files.has_key(branch):
+                    branch_files[branch].append(file)
+                else:
+                    branch_files[branch] = [file]
+
+        for branch in branch_files:
+            c = changes.Change(who=who,
+                               files=branch_files[branch],
+                               comments=comments,
+                               revision=num,
+                               when=when,
+                               branch=branch)
+            self.parent.addChange(c)
+
+        self.last_change = num
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/pb.py
@@ -0,0 +1,108 @@
+# -*- test-case-name: buildbot.test.test_changes -*-
+
+from twisted.python import log
+
+from buildbot.pbutil import NewCredPerspective
+from buildbot.changes import base, changes
+
+class ChangePerspective(NewCredPerspective):
+
+    def __init__(self, changemaster, prefix):
+        self.changemaster = changemaster
+        self.prefix = prefix
+
+    def attached(self, mind):
+        return self
+    def detached(self, mind):
+        pass
+
+    def perspective_addChange(self, changedict):
+        log.msg("perspective_addChange called")
+        pathnames = []
+        prefixpaths = None
+        for path in changedict['files']:
+            if self.prefix:
+                if not path.startswith(self.prefix):
+                    # this file does not start with the prefix, so ignore it
+                    continue
+                path = path[len(self.prefix):]
+            pathnames.append(path)
+
+        if pathnames:
+            change = changes.Change(changedict['who'],
+                                    pathnames,
+                                    changedict['comments'],
+                                    branch=changedict.get('branch'),
+                                    revision=changedict.get('revision'),
+                                    )
+            self.changemaster.addChange(change)
+
+class PBChangeSource(base.ChangeSource):
+    compare_attrs = ["user", "passwd", "port", "prefix"]
+
+    def __init__(self, user="change", passwd="changepw", port=None,
+                 prefix=None, sep=None):
+        """I listen on a TCP port for Changes from 'buildbot sendchange'.
+
+        I am a ChangeSource which will accept Changes from a remote source. I
+        share a TCP listening port with the buildslaves.
+
+        Both the 'buildbot sendchange' command and the
+        contrib/svn_buildbot.py tool know how to send changes to me.
+
+        @type prefix: string (or None)
+        @param prefix: if set, I will ignore any filenames that do not start
+                       with this string. Moreover I will remove this string
+                       from all filenames before creating the Change object
+                       and delivering it to the Schedulers. This is useful
+                       for changes coming from version control systems that
+                       represent branches as parent directories within the
+                       repository (like SVN and Perforce). Use a prefix of
+                       'trunk/' or 'project/branches/foobranch/' to only
+                       follow one branch and to get correct tree-relative
+                       filenames.
+
+        @param sep: DEPRECATED (with an axe). sep= was removed in
+                    buildbot-0.7.4 . Instead of using it, you should use
+                    prefix= with a trailing directory separator. This
+                    docstring (and the better-than-nothing error message
+                    which occurs when you use it) will be removed in 0.7.5 .
+        """
+
+        # sep= was removed in 0.7.4 . This more-helpful-than-nothing error
+        # message will be removed in 0.7.5 .
+        assert sep is None, "prefix= is now a complete string, do not use sep="
+        # TODO: current limitations
+        assert user == "change"
+        assert passwd == "changepw"
+        assert port == None
+        self.user = user
+        self.passwd = passwd
+        self.port = port
+        self.prefix = prefix
+
+    def describe(self):
+        # TODO: when the dispatcher is fixed, report the specific port
+        #d = "PB listener on port %d" % self.port
+        d = "PBChangeSource listener on all-purpose slaveport"
+        if self.prefix is not None:
+            d += " (prefix '%s')" % self.prefix
+        return d
+
+    def startService(self):
+        base.ChangeSource.startService(self)
+        # our parent is the ChangeMaster object
+        # find the master's Dispatch object and register our username
+        # TODO: the passwd should be registered here too
+        master = self.parent.parent
+        master.dispatcher.register(self.user, self)
+
+    def stopService(self):
+        base.ChangeSource.stopService(self)
+        # unregister our username
+        master = self.parent.parent
+        master.dispatcher.unregister(self.user)
+
+    def getPerspective(self):
+        return ChangePerspective(self.parent, self.prefix)
+
new file mode 100644
--- /dev/null
+++ b/buildbot/changes/svnpoller.py
@@ -0,0 +1,460 @@
+# -*- test-case-name: buildbot.test.test_svnpoller -*-
+
+# Based on the work of Dave Peticolas for the P4poll
+# Changed to svn (using xml.dom.minidom) by Niklaus Giger
+# Hacked beyond recognition by Brian Warner
+
+from twisted.python import log
+from twisted.internet import defer, reactor, utils
+from twisted.internet.task import LoopingCall
+
+from buildbot import util
+from buildbot.changes import base
+from buildbot.changes.changes import Change
+
+import xml.dom.minidom
+
+def _assert(condition, msg):
+    if condition:
+        return True
+    raise AssertionError(msg)
+
+def dbgMsg(myString):
+    log.msg(myString)
+    return 1
+
+# these split_file_* functions are available for use as values to the
+# split_file= argument.
+def split_file_alwaystrunk(path):
+    return (None, path)
+
+def split_file_branches(path):
+    # turn trunk/subdir/file.c into (None, "subdir/file.c")
+    # and branches/1.5.x/subdir/file.c into ("branches/1.5.x", "subdir/file.c")
+    pieces = path.split('/')
+    if pieces[0] == 'trunk':
+        return (None, '/'.join(pieces[1:]))
+    elif pieces[0] == 'branches':
+        return ('/'.join(pieces[0:2]), '/'.join(pieces[2:]))
+    else:
+        return None
+
+
+class SVNPoller(base.ChangeSource, util.ComparableMixin):
+    """This source will poll a Subversion repository for changes and submit
+    them to the change master."""
+
+    compare_attrs = ["svnurl", "split_file_function",
+                     "svnuser", "svnpasswd",
+                     "pollinterval", "histmax",
+                     "svnbin"]
+
+    parent = None # filled in when we're added
+    last_change = None
+    loop = None
+    working = False
+
+    def __init__(self, svnurl, split_file=None,
+                 svnuser=None, svnpasswd=None,
+                 pollinterval=10*60, histmax=100,
+                 svnbin='svn'):
+        """
+        @type  svnurl: string
+        @param svnurl: the SVN URL that describes the repository and
+                       subdirectory to watch. If this ChangeSource should
+                       only pay attention to a single branch, this should
+                       point at the repository for that branch, like
+                       svn://svn.twistedmatrix.com/svn/Twisted/trunk . If it
+                       should follow multiple branches, point it at the
+                       repository directory that contains all the branches
+                       like svn://svn.twistedmatrix.com/svn/Twisted and also
+                       provide a branch-determining function.
+
+                       Each file in the repository has a SVN URL in the form
+                       (SVNURL)/(BRANCH)/(FILEPATH), where (BRANCH) could be
+                       empty or not, depending upon your branch-determining
+                       function. Only files that start with (SVNURL)/(BRANCH)
+                       will be monitored. The Change objects that are sent to
+                       the Schedulers will see (FILEPATH) for each modified
+                       file.
+
+        @type  split_file: callable or None
+        @param split_file: a function that is called with a string of the
+                           form (BRANCH)/(FILEPATH) and should return a tuple
+                           (BRANCH, FILEPATH). This function should match
+                           your repository's branch-naming policy. Each
+                           changed file has a fully-qualified URL that can be
+                           split into a prefix (which equals the value of the
+                           'svnurl' argument) and a suffix; it is this suffix
+                           which is passed to the split_file function.
+
+                           If the function returns None, the file is ignored.
+                           Use this to indicate that the file is not a part
+                           of this project.
+                           
+                           For example, if your repository puts the trunk in
+                           trunk/... and branches are in places like
+                           branches/1.5/..., your split_file function could
+                           look like the following (this function is
+                           available as svnpoller.split_file_branches)::
+
+                            pieces = path.split('/')
+                            if pieces[0] == 'trunk':
+                                return (None, '/'.join(pieces[1:]))
+                            elif pieces[0] == 'branches':
+                                return ('/'.join(pieces[0:2]),
+                                        '/'.join(pieces[2:]))
+                            else:
+                                return None
+
+                           If instead your repository layout puts the trunk
+                           for ProjectA in trunk/ProjectA/... and the 1.5
+                           branch in branches/1.5/ProjectA/..., your
+                           split_file function could look like::
+
+                            pieces = path.split('/')
+                            if pieces[0] == 'trunk':
+                                branch = None
+                                pieces.pop(0) # remove 'trunk'
+                            elif pieces[0] == 'branches':
+                                pieces.pop(0) # remove 'branches'
+                                # grab branch name
+                                branch = 'branches/' + pieces.pop(0)
+                            else:
+                                return None # something weird
+                            projectname = pieces.pop(0)
+                            if projectname != 'ProjectA':
+                                return None # wrong project
+                            return (branch, '/'.join(pieces))
+
+                           The default of split_file= is None, which
+                           indicates that no splitting should be done. This
+                           is equivalent to the following function::
+
+                            return (None, path)
+
+                           If you wish, you can override the split_file
+                           method with the same sort of function instead of
+                           passing in a split_file= argument.
+
+
+        @type  svnuser:      string
+        @param svnuser:      If set, the --username option will be added to
+                             the 'svn log' command. You may need this to get
+                             access to a private repository.
+        @type  svnpasswd:    string
+        @param svnpasswd:    If set, the --password option will be added.
+
+        @type  pollinterval: int
+        @param pollinterval: interval in seconds between polls. The default
+                             is 600 seconds (10 minutes). Smaller values
+                             decrease the latency between the time a change
+                             is recorded and the time the buildbot notices
+                             it, but it also increases the system load.
+
+        @type  histmax:      int
+        @param histmax:      maximum number of changes to look back through.
+                             The default is 100. Smaller values decrease
+                             system load, but if more than histmax changes
+                             are recorded between polls, the extra ones will
+                             be silently lost.
+
+        @type  svnbin:       string
+        @param svnbin:       path to svn binary, defaults to just 'svn'. Use
+                             this if your subversion command lives in an
+                             unusual location.
+        """
+
+        if svnurl.endswith("/"):
+            svnurl = svnurl[:-1] # strip the trailing slash
+        self.svnurl = svnurl
+        self.split_file_function = split_file or split_file_alwaystrunk
+        self.svnuser = svnuser
+        self.svnpasswd = svnpasswd
+
+        self.svnbin = svnbin
+        self.pollinterval = pollinterval
+        self.histmax = histmax
+        self._prefix = None
+        self.overrun_counter = 0
+        self.loop = LoopingCall(self.checksvn)
+
+    def split_file(self, path):
+        # use getattr() to avoid turning this function into a bound method,
+        # which would require it to have an extra 'self' argument
+        f = getattr(self, "split_file_function")
+        return f(path)
+
+    def startService(self):
+        log.msg("SVNPoller(%s) starting" % self.svnurl)
+        base.ChangeSource.startService(self)
+        # Don't start the loop just yet because the reactor isn't running.
+        # Give it a chance to go and install our SIGCHLD handler before
+        # spawning processes.
+        reactor.callLater(0, self.loop.start, self.pollinterval)
+
+    def stopService(self):
+        log.msg("SVNPoller(%s) shutting down" % self.svnurl)
+        self.loop.stop()
+        return base.ChangeSource.stopService(self)
+
+    def describe(self):
+        return "SVNPoller watching %s" % self.svnurl
+
+    def checksvn(self):
+        # Our return value is only used for unit testing.
+
+        # we need to figure out the repository root, so we can figure out
+        # repository-relative pathnames later. Each SVNURL is in the form
+        # (ROOT)/(PROJECT)/(BRANCH)/(FILEPATH), where (ROOT) is something
+        # like svn://svn.twistedmatrix.com/svn/Twisted (i.e. there is a
+        # physical repository at /svn/Twisted on that host), (PROJECT) is
+        # something like Projects/Twisted (i.e. within the repository's
+        # internal namespace, everything under Projects/Twisted/ has
+        # something to do with Twisted, but these directory names do not
+        # actually appear on the repository host), (BRANCH) is something like
+        # "trunk" or "branches/2.0.x", and (FILEPATH) is a tree-relative
+        # filename like "twisted/internet/defer.py".
+
+        # our self.svnurl attribute contains (ROOT)/(PROJECT) combined
+        # together in a way that we can't separate without svn's help. If the
+        # user is not using the split_file= argument, then self.svnurl might
+        # be (ROOT)/(PROJECT)/(BRANCH) . In any case, the filenames we will
+        # get back from 'svn log' will be of the form
+        # (PROJECT)/(BRANCH)/(FILEPATH), but we want to be able to remove
+        # that (PROJECT) prefix from them. To do this without requiring the
+        # user to tell us how svnurl is split into ROOT and PROJECT, we do an
+        # 'svn info --xml' command at startup. This command will include a
+        # <root> element that tells us ROOT. We then strip this prefix from
+        # self.svnurl to determine PROJECT, and then later we strip the
+        # PROJECT prefix from the filenames reported by 'svn log --xml' to
+        # get a (BRANCH)/(FILEPATH) that can be passed to split_file() to
+        # turn into separate BRANCH and FILEPATH values.
+
+        # whew.
+
+        if self.working:
+            log.msg("SVNPoller(%s) overrun: timer fired but the previous "
+                    "poll had not yet finished." % self.svnurl)
+            self.overrun_counter += 1
+            return defer.succeed(None)
+        self.working = True
+
+        log.msg("SVNPoller polling")
+        if not self._prefix:
+            # this sets self._prefix when it finishes. It fires with
+            # self._prefix as well, because that makes the unit tests easier
+            # to write.
+            d = self.get_root()
+            d.addCallback(self.determine_prefix)
+        else:
+            d = defer.succeed(self._prefix)
+
+        d.addCallback(self.get_logs)
+        d.addCallback(self.parse_logs)
+        d.addCallback(self.get_new_logentries)
+        d.addCallback(self.create_changes)
+        d.addCallback(self.submit_changes)
+        d.addCallbacks(self.finished_ok, self.finished_failure)
+        return d
+
+    def getProcessOutput(self, args):
+        # this exists so we can override it during the unit tests
+        d = utils.getProcessOutput(self.svnbin, args, {})
+        return d
+
+    def get_root(self):
+        args = ["info", "--xml", "--non-interactive", self.svnurl]
+        if self.svnuser:
+            args.extend(["--username=%s" % self.svnuser])
+        if self.svnpasswd:
+            args.extend(["--password=%s" % self.svnpasswd])
+        d = self.getProcessOutput(args)
+        return d
+
+    def determine_prefix(self, output):
+        try:
+            doc = xml.dom.minidom.parseString(output)
+        except xml.parsers.expat.ExpatError:
+            dbgMsg("_process_changes: ExpatError in %s" % output)
+            log.msg("SVNPoller._determine_prefix_2: ExpatError in '%s'"
+                    % output)
+            raise
+        rootnodes = doc.getElementsByTagName("root")
+        if not rootnodes:
+            # this happens if the URL we gave was already the root. In this
+            # case, our prefix is empty.
+            self._prefix = ""
+            return self._prefix
+        rootnode = rootnodes[0]
+        root = "".join([c.data for c in rootnode.childNodes])
+        # root will be a unicode string
+        _assert(self.svnurl.startswith(root),
+                "svnurl='%s' doesn't start with <root>='%s'" %
+                (self.svnurl, root))
+        self._prefix = self.svnurl[len(root):]
+        if self._prefix.startswith("/"):
+            self._prefix = self._prefix[1:]
+        log.msg("SVNPoller: svnurl=%s, root=%s, so prefix=%s" %
+                (self.svnurl, root, self._prefix))
+        return self._prefix
+
+    def get_logs(self, ignored_prefix=None):
+        args = []
+        args.extend(["log", "--xml", "--verbose", "--non-interactive"])
+        if self.svnuser:
+            args.extend(["--username=%s" % self.svnuser])
+        if self.svnpasswd:
+            args.extend(["--password=%s" % self.svnpasswd])
+        args.extend(["--limit=%d" % (self.histmax), self.svnurl])
+        d = self.getProcessOutput(args)
+        return d
+
+    def parse_logs(self, output):
+        # parse the XML output, return a list of <logentry> nodes
+        try:
+            doc = xml.dom.minidom.parseString(output)
+        except xml.parsers.expat.ExpatError:
+            dbgMsg("_process_changes: ExpatError in %s" % output)
+            log.msg("SVNPoller._parse_changes: ExpatError in '%s'" % output)
+            raise
+        logentries = doc.getElementsByTagName("logentry")
+        return logentries
+
+
+    def _filter_new_logentries(self, logentries, last_change):
+        # given a list of logentries, return a tuple of (new_last_change,
+        # new_logentries), where new_logentries contains only the ones after
+        # last_change
+        if not logentries:
+            # no entries, so last_change must stay at None
+            return (None, [])
+
+        mostRecent = int(logentries[0].getAttribute("revision"))
+
+        if last_change is None:
+            # if this is the first time we've been run, ignore any changes
+            # that occurred before now. This prevents a build at every
+            # startup.
+            log.msg('svnPoller: starting at change %s' % mostRecent)
+            return (mostRecent, [])
+
+        if last_change == mostRecent:
+            # an unmodified repository will hit this case
+            log.msg('svnPoller: _process_changes last %s mostRecent %s' % (
+                      last_change, mostRecent))
+            return (mostRecent, [])
+
+        new_logentries = []
+        for el in logentries:
+            if last_change == int(el.getAttribute("revision")):
+                break
+            new_logentries.append(el)
+        new_logentries.reverse() # return oldest first
+        return (mostRecent, new_logentries)
+
+    def get_new_logentries(self, logentries):
+        last_change = self.last_change
+        (new_last_change,
+         new_logentries) = self._filter_new_logentries(logentries,
+                                                       self.last_change)
+        self.last_change = new_last_change
+        log.msg('svnPoller: _process_changes %s .. %s' %
+                (last_change, new_last_change))
+        return new_logentries
+
+
+    def _get_text(self, element, tag_name):
+        child_nodes = element.getElementsByTagName(tag_name)[0].childNodes
+        text = "".join([t.data for t in child_nodes])
+        return text
+
+    def _transform_path(self, path):
+        _assert(path.startswith(self._prefix),
+                "filepath '%s' should start with prefix '%s'" %
+                (path, self._prefix))
+        relative_path = path[len(self._prefix):]
+        if relative_path.startswith("/"):
+            relative_path = relative_path[1:]
+        where = self.split_file(relative_path)
+        # 'where' is either None or (branch, final_path)
+        return where
+
+    def create_changes(self, new_logentries):
+        changes = []
+
+        for el in new_logentries:
+            branch_files = [] # get oldest change first
+            revision = str(el.getAttribute("revision"))
+            dbgMsg("Adding change revision %s" % (revision,))
+            # TODO: the rest of buildbot may not be ready for unicode 'who'
+            # values
+            author   = self._get_text(el, "author")
+            comments = self._get_text(el, "msg")
+            # there is a "date" field, but it provides localtime in the
+            # repository's timezone, whereas we care about buildmaster's
+            # localtime (since this will get used to position the boxes on
+            # the Waterfall display, etc). So ignore the date field and use
+            # our local clock instead.
+            #when     = self._get_text(el, "date")
+            #when     = time.mktime(time.strptime("%.19s" % when,
+            #                                     "%Y-%m-%dT%H:%M:%S"))
+            branches = {}
+            pathlist = el.getElementsByTagName("paths")[0]
+            for p in pathlist.getElementsByTagName("path"):
+                action = p.getAttribute("action")
+                path = "".join([t.data for t in p.childNodes])
+                # the rest of buildbot is certaily not yet ready to handle
+                # unicode filenames, because they get put in RemoteCommands
+                # which get sent via PB to the buildslave, and PB doesn't
+                # handle unicode.
+                path = path.encode("ascii")
+                if path.startswith("/"):
+                    path = path[1:]
+                where = self._transform_path(path)
+
+                # if 'where' is None, the file was outside any project that
+                # we care about and we should ignore it
+                if where:
+                    branch, filename = where
+                    if not branch in branches:
+                        branches[branch] = { 'files': []}
+                    branches[branch]['files'].append(filename)
+
+                    if not branches[branch].has_key('action'):
+                        branches[branch]['action'] = action
+
+            for branch in branches.keys():
+                action = branches[branch]['action']
+                files  = branches[branch]['files']
+                number_of_files_changed = len(files)
+
+                if action == u'D' and number_of_files_changed == 1 and files[0] == '':
+                    log.msg("Ignoring deletion of branch '%s'" % branch)
+                else:
+                    c = Change(who=author,
+                               files=files,
+                               comments=comments,
+                               revision=revision,
+                               branch=branch)
+                    changes.append(c)
+
+        return changes
+
+    def submit_changes(self, changes):
+        for c in changes:
+            self.parent.addChange(c)
+
+    def finished_ok(self, res):
+        log.msg("SVNPoller finished polling")
+        dbgMsg('_finished : %s' % res)
+        assert self.working
+        self.working = False
+        return res
+
+    def finished_failure(self, f):
+        log.msg("SVNPoller failed")
+        dbgMsg('_finished : %s' % f)
+        assert self.working
+        self.working = False
+        return None # eat the failure
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/buildbot/clients/base.py
@@ -0,0 +1,125 @@
+
+import sys, re
+
+from twisted.spread import pb
+from twisted.cred import credentials, error
+from twisted.internet import reactor
+
+class StatusClient(pb.Referenceable):
+    """To use this, call my .connected method with a RemoteReference to the
+    buildmaster's StatusClientPerspective object.
+    """
+
+    def __init__(self, events):
+        self.builders = {}
+        self.events = events
+
+    def connected(self, remote):
+        print "connected"
+        self.remote = remote
+        remote.callRemote("subscribe", self.events, 5, self)
+
+    def remote_builderAdded(self, buildername, builder):
+        print "builderAdded", buildername
+
+    def remote_builderRemoved(self, buildername):
+        print "builderRemoved", buildername
+
+    def remote_builderChangedState(self, buildername, state, eta):
+        print "builderChangedState", buildername, state, eta
+
+    def remote_buildStarted(self, buildername, build):
+        print "buildStarted", buildername
+
+    def remote_buildFinished(self, buildername, build, results):
+        print "buildFinished", results
+
+    def remote_buildETAUpdate(self, buildername, build, eta):
+        print "ETA", buildername, eta
+
+    def remote_stepStarted(self, buildername, build, stepname, step):
+        print "stepStarted", buildername, stepname
+
+    def remote_stepFinished(self, buildername, build, stepname, step, results):
+        print "stepFinished", buildername, stepname, results
+
+    def remote_stepETAUpdate(self, buildername, build, stepname, step,
+                             eta, expectations):
+        print "stepETA", buildername, stepname, eta
+
+    def remote_logStarted(self, buildername, build, stepname, step,
+                          logname, log):
+        print "logStarted", buildername, stepname
+
+    def remote_logFinished(self, buildername, build, stepname, step,
+                           logname, log):
+        print "logFinished", buildername, stepname
+
+    def remote_logChunk(self, buildername, build, stepname, step, logname, log,
+                        channel, text):
+        ChunkTypes = ["STDOUT", "STDERR", "HEADER"]
+        print "logChunk[%s]: %s" % (ChunkTypes[channel], text)
+
+class TextClient:
+    def __init__(self, master, events="steps"):
+        """
+        @type  events: string, one of builders, builds, steps, logs, full
+        @param events: specify what level of detail should be reported.
+         - 'builders': only announce new/removed Builders
+         - 'builds': also announce builderChangedState, buildStarted, and
+           buildFinished
+         - 'steps': also announce buildETAUpdate, stepStarted, stepFinished
+         - 'logs': also announce stepETAUpdate, logStarted, logFinished
+         - 'full': also announce log contents
+        """        
+        self.master = master
+        self.listener = StatusClient(events)
+
+    def run(self):
+        """Start the TextClient."""
+        self.startConnecting()
+        reactor.run()
+
+    def startConnecting(self):
+        try:
+            host, port = re.search(r'(.+):(\d+)', self.master).groups()
+            port = int(port)
+        except:
+            print "unparseable master location '%s'" % self.master
+            print " expecting something more like localhost:8007"
+            raise
+        cf = pb.PBClientFactory()
+        creds = credentials.UsernamePassword("statusClient", "clientpw")
+        d = cf.login(creds)
+        reactor.connectTCP(host, port, cf)
+        d.addCallbacks(self.connected, self.not_connected)
+        return d
+    def connected(self, ref):
+        ref.notifyOnDisconnect(self.disconnected)
+        self.listener.connected(ref)
+    def not_connected(self, why):
+        if why.check(error.UnauthorizedLogin):
+            print """
+Unable to login.. are you sure we are connecting to a
+buildbot.status.client.PBListener port and not to the slaveport?
+"""
+        reactor.stop()
+        return why
+    def disconnected(self, ref):
+        print "lost connection"
+        # we can get here in one of two ways: the buildmaster has
+        # disconnected us (probably because it shut itself down), or because
+        # we've been SIGINT'ed. In the latter case, our reactor is already
+        # shut down, but we have no easy way of detecting that. So protect
+        # our attempt to shut down the reactor.
+        try:
+            reactor.stop()
+        except RuntimeError:
+            pass
+
+if __name__ == '__main__':
+    master = "localhost:8007"
+    if len(sys.argv) > 1:
+        master = sys.argv[1]
+    c = TextClient()
+    c.run()
new file mode 100644
--- /dev/null
+++ b/buildbot/clients/debug.glade
@@ -0,0 +1,684 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+<requires lib="gnome"/>
+
+<widget class="GtkWindow" id="window1">
+  <property name="visible">True</property>
+  <property name="title" translatable="yes">Buildbot Debug Tool</property>
+  <property name="type">GTK_WINDOW_TOPLEVEL</property>
+  <property name="window_position">GTK_WIN_POS_NONE</property>
+  <property name="modal">False</property>
+  <property name="resizable">True</property>
+  <property name="destroy_with_parent">False</property>
+  <property name="decorated">True</property>
+  <property name="skip_taskbar_hint">False</property>
+  <property name="skip_pager_hint">False</property>
+  <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
+  <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
+  <property name="focus_on_map">True</property>
+  <property name="urgency_hint">False</property>
+
+  <child>
+    <widget class="GtkVBox" id="vbox1">
+      <property name="visible">True</property>
+      <property name="homogeneous">False</property>
+      <property name="spacing">0</property>
+
+      <child>
+	<widget class="GtkHBox" id="connection">
+	  <property name="visible">True</property>
+	  <property name="homogeneous">False</property>
+	  <property name="spacing">0</property>
+
+	  <child>
+	    <widget class="GtkButton" id="connectbutton">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label" translatable="yes">Connect</property>
+	      <property name="use_underline">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <signal name="clicked" handler="do_connect"/>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkLabel" id="connectlabel">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">Disconnected</property>
+	      <property name="use_underline">False</property>
+	      <property name="use_markup">False</property>
+	      <property name="justify">GTK_JUSTIFY_CENTER</property>
+	      <property name="wrap">False</property>
+	      <property name="selectable">False</property>
+	      <property name="xalign">0.5</property>
+	      <property name="yalign">0.5</property>
+	      <property name="xpad">0</property>
+	      <property name="ypad">0</property>
+	      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+	      <property name="width_chars">-1</property>
+	      <property name="single_line_mode">False</property>
+	      <property name="angle">0</property>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">True</property>
+	      <property name="fill">True</property>
+	    </packing>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkHBox" id="commands">
+	  <property name="visible">True</property>
+	  <property name="homogeneous">False</property>
+	  <property name="spacing">0</property>
+
+	  <child>
+	    <widget class="GtkButton" id="reload">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label" translatable="yes">Reload .cfg</property>
+	      <property name="use_underline">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <signal name="clicked" handler="do_reload" last_modification_time="Wed, 24 Sep 2003 20:47:55 GMT"/>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkButton" id="rebuild">
+	      <property name="visible">True</property>
+	      <property name="sensitive">False</property>
+	      <property name="can_focus">True</property>
+	      <property name="label" translatable="yes">Rebuild .py</property>
+	      <property name="use_underline">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <signal name="clicked" handler="do_rebuild" last_modification_time="Wed, 24 Sep 2003 20:49:18 GMT"/>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkButton" id="button7">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label" translatable="yes">poke IRC</property>
+	      <property name="use_underline">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <signal name="clicked" handler="do_poke_irc" last_modification_time="Wed, 14 Jan 2004 22:23:59 GMT"/>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">True</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkHBox" id="hbox3">
+	  <property name="visible">True</property>
+	  <property name="homogeneous">False</property>
+	  <property name="spacing">0</property>
+
+	  <child>
+	    <widget class="GtkCheckButton" id="usebranch">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label" translatable="yes">Branch:</property>
+	      <property name="use_underline">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="active">False</property>
+	      <property name="inconsistent">False</property>
+	      <property name="draw_indicator">True</property>
+	      <signal name="toggled" handler="on_usebranch_toggled" last_modification_time="Tue, 25 Oct 2005 01:42:45 GMT"/>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkEntry" id="branch">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="editable">True</property>
+	      <property name="visibility">True</property>
+	      <property name="max_length">0</property>
+	      <property name="text" translatable="yes"></property>
+	      <property name="has_frame">True</property>
+	      <property name="invisible_char">*</property>
+	      <property name="activates_default">False</property>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">True</property>
+	      <property name="fill">True</property>
+	    </packing>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">True</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkHBox" id="hbox1">
+	  <property name="visible">True</property>
+	  <property name="homogeneous">False</property>
+	  <property name="spacing">0</property>
+
+	  <child>
+	    <widget class="GtkCheckButton" id="userevision">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label" translatable="yes">Revision:</property>
+	      <property name="use_underline">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="active">False</property>
+	      <property name="inconsistent">False</property>
+	      <property name="draw_indicator">True</property>
+	      <signal name="toggled" handler="on_userevision_toggled" last_modification_time="Wed, 08 Sep 2004 17:58:33 GMT"/>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkEntry" id="revision">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="editable">True</property>
+	      <property name="visibility">True</property>
+	      <property name="max_length">0</property>
+	      <property name="text" translatable="yes"></property>
+	      <property name="has_frame">True</property>
+	      <property name="invisible_char">*</property>
+	      <property name="activates_default">False</property>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">True</property>
+	      <property name="fill">True</property>
+	    </packing>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">True</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkFrame" id="Commit">
+	  <property name="border_width">4</property>
+	  <property name="visible">True</property>
+	  <property name="label_xalign">0</property>
+	  <property name="label_yalign">0.5</property>
+	  <property name="shadow_type">GTK_SHADOW_ETCHED_IN</property>
+
+	  <child>
+	    <widget class="GtkAlignment" id="alignment1">
+	      <property name="visible">True</property>
+	      <property name="xalign">0.5</property>
+	      <property name="yalign">0.5</property>
+	      <property name="xscale">1</property>
+	      <property name="yscale">1</property>
+	      <property name="top_padding">0</property>
+	      <property name="bottom_padding">0</property>
+	      <property name="left_padding">0</property>
+	      <property name="right_padding">0</property>
+
+	      <child>
+		<widget class="GtkVBox" id="vbox3">
+		  <property name="visible">True</property>
+		  <property name="homogeneous">False</property>
+		  <property name="spacing">0</property>
+
+		  <child>
+		    <widget class="GtkHBox" id="commit">
+		      <property name="visible">True</property>
+		      <property name="homogeneous">False</property>
+		      <property name="spacing">0</property>
+
+		      <child>
+			<widget class="GtkButton" id="button2">
+			  <property name="visible">True</property>
+			  <property name="can_focus">True</property>
+			  <property name="label" translatable="yes">commit</property>
+			  <property name="use_underline">True</property>
+			  <property name="relief">GTK_RELIEF_NORMAL</property>
+			  <property name="focus_on_click">True</property>
+			  <signal name="clicked" handler="do_commit"/>
+			</widget>
+			<packing>
+			  <property name="padding">0</property>
+			  <property name="expand">False</property>
+			  <property name="fill">False</property>
+			</packing>
+		      </child>
+
+		      <child>
+			<widget class="GtkEntry" id="filename">
+			  <property name="visible">True</property>
+			  <property name="can_focus">True</property>
+			  <property name="editable">True</property>
+			  <property name="visibility">True</property>
+			  <property name="max_length">0</property>
+			  <property name="text" translatable="yes">twisted/internet/app.py</property>
+			  <property name="has_frame">True</property>
+			  <property name="invisible_char">*</property>
+			  <property name="activates_default">False</property>
+			</widget>
+			<packing>
+			  <property name="padding">0</property>
+			  <property name="expand">True</property>
+			  <property name="fill">True</property>
+			</packing>
+		      </child>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">True</property>
+		      <property name="fill">True</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkHBox" id="hbox2">
+		      <property name="visible">True</property>
+		      <property name="homogeneous">False</property>
+		      <property name="spacing">0</property>
+
+		      <child>
+			<widget class="GtkLabel" id="label5">
+			  <property name="visible">True</property>
+			  <property name="label" translatable="yes">Who: </property>
+			  <property name="use_underline">False</property>
+			  <property name="use_markup">False</property>
+			  <property name="justify">GTK_JUSTIFY_LEFT</property>
+			  <property name="wrap">False</property>
+			  <property name="selectable">False</property>
+			  <property name="xalign">0.5</property>
+			  <property name="yalign">0.5</property>
+			  <property name="xpad">0</property>
+			  <property name="ypad">0</property>
+			  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+			  <property name="width_chars">-1</property>
+			  <property name="single_line_mode">False</property>
+			  <property name="angle">0</property>
+			</widget>
+			<packing>
+			  <property name="padding">0</property>
+			  <property name="expand">False</property>
+			  <property name="fill">False</property>
+			</packing>
+		      </child>
+
+		      <child>
+			<widget class="GtkEntry" id="who">
+			  <property name="visible">True</property>
+			  <property name="can_focus">True</property>
+			  <property name="editable">True</property>
+			  <property name="visibility">True</property>
+			  <property name="max_length">0</property>
+			  <property name="text" translatable="yes">bob</property>
+			  <property name="has_frame">True</property>
+			  <property name="invisible_char">*</property>
+			  <property name="activates_default">False</property>
+			</widget>
+			<packing>
+			  <property name="padding">0</property>
+			  <property name="expand">True</property>
+			  <property name="fill">True</property>
+			</packing>
+		      </child>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">True</property>
+		      <property name="fill">True</property>
+		    </packing>
+		  </child>
+		</widget>
+	      </child>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkLabel" id="label4">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">Commit</property>
+	      <property name="use_underline">False</property>
+	      <property name="use_markup">False</property>
+	      <property name="justify">GTK_JUSTIFY_LEFT</property>
+	      <property name="wrap">False</property>
+	      <property name="selectable">False</property>
+	      <property name="xalign">0.5</property>
+	      <property name="yalign">0.5</property>
+	      <property name="xpad">2</property>
+	      <property name="ypad">0</property>
+	      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+	      <property name="width_chars">-1</property>
+	      <property name="single_line_mode">False</property>
+	      <property name="angle">0</property>
+	    </widget>
+	    <packing>
+	      <property name="type">label_item</property>
+	    </packing>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">True</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkFrame" id="builderframe">
+	  <property name="border_width">4</property>
+	  <property name="visible">True</property>
+	  <property name="label_xalign">0</property>
+	  <property name="label_yalign">0.5</property>
+	  <property name="shadow_type">GTK_SHADOW_ETCHED_IN</property>
+
+	  <child>
+	    <widget class="GtkVBox" id="vbox2">
+	      <property name="visible">True</property>
+	      <property name="homogeneous">False</property>
+	      <property name="spacing">0</property>
+
+	      <child>
+		<widget class="GtkHBox" id="builder">
+		  <property name="visible">True</property>
+		  <property name="homogeneous">False</property>
+		  <property name="spacing">3</property>
+
+		  <child>
+		    <widget class="GtkLabel" id="label1">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">Builder:</property>
+		      <property name="use_underline">False</property>
+		      <property name="use_markup">False</property>
+		      <property name="justify">GTK_JUSTIFY_CENTER</property>
+		      <property name="wrap">False</property>
+		      <property name="selectable">False</property>
+		      <property name="xalign">0.5</property>
+		      <property name="yalign">0.5</property>
+		      <property name="xpad">0</property>
+		      <property name="ypad">0</property>
+		      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		      <property name="width_chars">-1</property>
+		      <property name="single_line_mode">False</property>
+		      <property name="angle">0</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkEntry" id="buildname">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="editable">True</property>
+		      <property name="visibility">True</property>
+		      <property name="max_length">0</property>
+		      <property name="text" translatable="yes">one</property>
+		      <property name="has_frame">True</property>
+		      <property name="invisible_char">*</property>
+		      <property name="activates_default">False</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">True</property>
+		      <property name="fill">True</property>
+		    </packing>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">True</property>
+		  <property name="fill">True</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkHBox" id="buildercontrol">
+		  <property name="visible">True</property>
+		  <property name="homogeneous">False</property>
+		  <property name="spacing">0</property>
+
+		  <child>
+		    <widget class="GtkButton" id="button1">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="label" translatable="yes">Request
+Build</property>
+		      <property name="use_underline">True</property>
+		      <property name="relief">GTK_RELIEF_NORMAL</property>
+		      <property name="focus_on_click">True</property>
+		      <signal name="clicked" handler="do_build"/>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkButton" id="button8">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="label" translatable="yes">Ping
+Builder</property>
+		      <property name="use_underline">True</property>
+		      <property name="relief">GTK_RELIEF_NORMAL</property>
+		      <property name="focus_on_click">True</property>
+		      <signal name="clicked" handler="do_ping" last_modification_time="Fri, 24 Nov 2006 05:18:51 GMT"/>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <placeholder/>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">True</property>
+		  <property name="fill">True</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkHBox" id="status">
+		  <property name="visible">True</property>
+		  <property name="homogeneous">False</property>
+		  <property name="spacing">0</property>
+
+		  <child>
+		    <widget class="GtkLabel" id="label2">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">Currently:</property>
+		      <property name="use_underline">False</property>
+		      <property name="use_markup">False</property>
+		      <property name="justify">GTK_JUSTIFY_CENTER</property>
+		      <property name="wrap">False</property>
+		      <property name="selectable">False</property>
+		      <property name="xalign">0.5</property>
+		      <property name="yalign">0.5</property>
+		      <property name="xpad">7</property>
+		      <property name="ypad">0</property>
+		      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		      <property name="width_chars">-1</property>
+		      <property name="single_line_mode">False</property>
+		      <property name="angle">0</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkButton" id="button3">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="label" translatable="yes">offline</property>
+		      <property name="use_underline">True</property>
+		      <property name="relief">GTK_RELIEF_NORMAL</property>
+		      <property name="focus_on_click">True</property>
+		      <signal name="clicked" handler="do_current_offline"/>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkButton" id="button4">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="label" translatable="yes">idle</property>
+		      <property name="use_underline">True</property>
+		      <property name="relief">GTK_RELIEF_NORMAL</property>
+		      <property name="focus_on_click">True</property>
+		      <signal name="clicked" handler="do_current_idle"/>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkButton" id="button5">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="label" translatable="yes">waiting</property>
+		      <property name="use_underline">True</property>
+		      <property name="relief">GTK_RELIEF_NORMAL</property>
+		      <property name="focus_on_click">True</property>
+		      <signal name="clicked" handler="do_current_waiting"/>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkButton" id="button6">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="label" translatable="yes">building</property>
+		      <property name="use_underline">True</property>
+		      <property name="relief">GTK_RELIEF_NORMAL</property>
+		      <property name="focus_on_click">True</property>
+		      <signal name="clicked" handler="do_current_building"/>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">True</property>
+		  <property name="fill">True</property>
+		</packing>
+	      </child>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkLabel" id="label3">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">Builder</property>
+	      <property name="use_underline">False</property>
+	      <property name="use_markup">False</property>
+	      <property name="justify">GTK_JUSTIFY_LEFT</property>
+	      <property name="wrap">False</property>
+	      <property name="selectable">False</property>
+	      <property name="xalign">0.5</property>
+	      <property name="yalign">0.5</property>
+	      <property name="xpad">2</property>
+	      <property name="ypad">0</property>
+	      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+	      <property name="width_chars">-1</property>
+	      <property name="single_line_mode">False</property>
+	      <property name="angle">0</property>
+	    </widget>
+	    <packing>
+	      <property name="type">label_item</property>
+	    </packing>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">True</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+    </widget>
+  </child>
+</widget>
+
+</glade-interface>
new file mode 100644
--- /dev/null
+++ b/buildbot/clients/debug.py
@@ -0,0 +1,181 @@
+
+from twisted.internet import gtk2reactor
+gtk2reactor.install()
+from twisted.internet import reactor
+from twisted.python import util
+from twisted.spread import pb
+from twisted.cred import credentials
+import gtk.glade
+import sys, re
+
+class DebugWidget:
+    def __init__(self, master="localhost:8007", passwd="debugpw"):
+        self.connected = 0
+        try:
+            host, port = re.search(r'(.+):(\d+)', master).groups()
+        except:
+            print "unparseable master location '%s'" % master
+            print " expecting something more like localhost:8007"
+            raise
+        self.host = host
+        self.port = int(port)
+        self.passwd = passwd
+        self.remote = None
+        xml = self.xml = gtk.glade.XML(util.sibpath(__file__, "debug.glade"))
+        g = xml.get_widget
+        self.buildname = g('buildname')
+        self.filename = g('filename')
+        self.connectbutton = g('connectbutton')
+        self.connectlabel = g('connectlabel')
+        g('window1').connect('destroy', lambda win: gtk.main_quit())
+        # put the master info in the window's titlebar
+        g('window1').set_title("Buildbot Debug Tool: %s" % master)
+        c = xml.signal_connect
+        c('do_connect', self.do_connect)
+        c('do_reload', self.do_reload)
+        c('do_rebuild', self.do_rebuild)
+        c('do_poke_irc', self.do_poke_irc)
+        c('do_build', self.do_build)
+        c('do_ping', self.do_ping)
+        c('do_commit', self.do_commit)
+        c('on_usebranch_toggled', self.usebranch_toggled)
+        self.usebranch_toggled(g('usebranch'))
+        c('on_userevision_toggled', self.userevision_toggled)
+        self.userevision_toggled(g('userevision'))
+        c('do_current_offline', self.do_current, "offline")
+        c('do_current_idle', self.do_current, "idle")
+        c('do_current_waiting', self.do_current, "waiting")
+        c('do_current_building', self.do_current, "building")
+
+    def do_connect(self, widget):
+        if self.connected:
+            self.connectlabel.set_text("Disconnecting...")
+            if self.remote:
+                self.remote.broker.transport.loseConnection()
+        else:
+            self.connectlabel.set_text("Connecting...")
+            f = pb.PBClientFactory()
+            creds = credentials.UsernamePassword("debug", self.passwd)
+            d = f.login(creds)
+            reactor.connectTCP(self.host, int(self.port), f)
+            d.addCallbacks(self.connect_complete, self.connect_failed)
+    def connect_complete(self, ref):
+        self.connectbutton.set_label("Disconnect")
+        self.connectlabel.set_text("Connected")
+        self.connected = 1
+        self.remote = ref
+        self.remote.callRemote("print", "hello cleveland")
+        self.remote.notifyOnDisconnect(self.disconnected)
+    def connect_failed(self, why):
+        self.connectlabel.set_text("Failed")
+        print why
+    def disconnected(self, ref):
+        self.connectbutton.set_label("Connect")
+        self.connectlabel.set_text("Disconnected")
+        self.connected = 0
+        self.remote = None
+
+    def do_reload(self, widget):
+        if not self.remote:
+            return
+        d = self.remote.callRemote("reload")
+        d.addErrback(self.err)
+    def do_rebuild(self, widget):
+        print "Not yet implemented"
+        return
+    def do_poke_irc(self, widget):
+        if not self.remote:
+            return
+        d = self.remote.callRemote("pokeIRC")
+        d.addErrback(self.err)
+
+    def do_build(self, widget):
+        if not self.remote:
+            return
+        name = self.buildname.get_text()
+        branch = None
+        if self.xml.get_widget("usebranch").get_active():
+            branch = self.xml.get_widget('branch').get_text()
+            if branch == '':
+                branch = None
+        revision = None
+        if self.xml.get_widget("userevision").get_active():
+            revision = self.xml.get_widget('revision').get_text()
+            if revision == '':
+                revision = None
+        reason = "debugclient 'Request Build' button pushed"
+        properties = {}
+        d = self.remote.callRemote("requestBuild",
+                                   name, reason, branch, revision, properties)
+        d.addErrback(self.err)
+
+    def do_ping(self, widget):
+        if not self.remote:
+            return
+        name = self.buildname.get_text()
+        d = self.remote.callRemote("pingBuilder", name)
+        d.addErrback(self.err)
+
+    def usebranch_toggled(self, widget):
+        rev = self.xml.get_widget('branch')
+        if widget.get_active():
+            rev.set_sensitive(True)
+        else:
+            rev.set_sensitive(False)
+
+    def userevision_toggled(self, widget):
+        rev = self.xml.get_widget('revision')
+        if widget.get_active():
+            rev.set_sensitive(True)
+        else:
+            rev.set_sensitive(False)
+
+    def do_commit(self, widget):
+        if not self.remote:
+            return
+        filename = self.filename.get_text()
+        who = self.xml.get_widget("who").get_text()
+
+        branch = None
+        if self.xml.get_widget("usebranch").get_active():
+            branch = self.xml.get_widget('branch').get_text()
+            if branch == '':
+                branch = None
+
+        revision = None
+        if self.xml.get_widget("userevision").get_active():
+            revision = self.xml.get_widget('revision').get_text()
+            try:
+                revision = int(revision)
+            except ValueError:
+                pass
+            if revision == '':
+                revision = None
+
+        kwargs = { 'revision': revision, 'who': who }
+        if branch:
+            kwargs['branch'] = branch
+        d = self.remote.callRemote("fakeChange", filename, **kwargs)
+        d.addErrback(self.err)
+
+    def do_current(self, widget, state):
+        if not self.remote:
+            return
+        name = self.buildname.get_text()
+        d = self.remote.callRemote("setCurrentState", name, state)
+        d.addErrback(self.err)
+    def err(self, failure):
+        print "received error:", failure
+
+    def run(self):
+        reactor.run()
+
+if __name__ == '__main__':
+    master = "localhost:8007"
+    if len(sys.argv) > 1:
+        master = sys.argv[1]
+    passwd = "debugpw"
+    if len(sys.argv) > 2:
+        passwd = sys.argv[2]
+    d = DebugWidget(master, passwd)
+    d.run()
new file mode 100644
--- /dev/null
+++ b/buildbot/clients/gtkPanes.py
@@ -0,0 +1,523 @@
+
+from twisted.internet import gtk2reactor
+gtk2reactor.install()
+
+import sys, time
+
+import pygtk
+pygtk.require("2.0")
+import gobject, gtk
+assert(gtk.Window) # in gtk1 it's gtk.GtkWindow
+
+from twisted.spread import pb
+
+#from buildbot.clients.base import Builder, Client
+from buildbot.clients.base import TextClient
+from buildbot.util import now
+
+'''
+class Pane:
+    def __init__(self):
+        pass
+
+class OneRow(Pane):
+    """This is a one-row status bar. It has one square per Builder, and that
+    square is either red, yellow, or green. """
+
+    def __init__(self):
+        Pane.__init__(self)
+        self.widget = gtk.VBox(gtk.FALSE, 2)
+        self.nameBox = gtk.HBox(gtk.TRUE)
+        self.statusBox = gtk.HBox(gtk.TRUE)
+        self.widget.add(self.nameBox)
+        self.widget.add(self.statusBox)
+        self.widget.show_all()
+        self.builders = []
+        
+    def getWidget(self):
+        return self.widget
+    def addBuilder(self, builder):
+        print "OneRow.addBuilder"
+        # todo: ordering. Should follow the order in which they were added
+        # to the original BotMaster
+        self.builders.append(builder)
+        # add the name to the left column, and a label (with background) to
+        # the right
+        name = gtk.Label(builder.name)
+        status = gtk.Label('??')
+        status.set_size_request(64,64)
+        box = gtk.EventBox()
+        box.add(status)
+        name.show()
+        box.show_all()
+        self.nameBox.add(name)
+        self.statusBox.add(box)
+        builder.haveSomeWidgets([name, status, box])
+    
+class R2Builder(Builder):
+    def start(self):
+        self.nameSquare.set_text(self.name)
+        self.statusSquare.set_text("???")
+        self.subscribe()
+    def haveSomeWidgets(self, widgets):
+        self.nameSquare, self.statusSquare, self.statusBox = widgets
+
+    def remote_newLastBuildStatus(self, event):
+        color = None
+        if event:
+            text = "\n".join(event.text)
+            color = event.color
+        else:
+            text = "none"
+        self.statusSquare.set_text(text)
+        if color:
+            print "color", color
+            self.statusBox.modify_bg(gtk.STATE_NORMAL,
+                                     gtk.gdk.color_parse(color))
+
+    def remote_currentlyOffline(self):
+        self.statusSquare.set_text("offline")
+    def remote_currentlyIdle(self):
+        self.statusSquare.set_text("idle")
+    def remote_currentlyWaiting(self, seconds):
+        self.statusSquare.set_text("waiting")
+    def remote_currentlyInterlocked(self):
+        self.statusSquare.set_text("interlocked")
+    def remote_currentlyBuilding(self, eta):
+        self.statusSquare.set_text("building")
+
+
+class CompactRow(Pane):
+    def __init__(self):
+        Pane.__init__(self)
+        self.widget = gtk.VBox(gtk.FALSE, 3)
+        self.nameBox = gtk.HBox(gtk.TRUE, 2)
+        self.lastBuildBox = gtk.HBox(gtk.TRUE, 2)
+        self.statusBox = gtk.HBox(gtk.TRUE, 2)
+        self.widget.add(self.nameBox)
+        self.widget.add(self.lastBuildBox)
+        self.widget.add(self.statusBox)
+        self.widget.show_all()
+        self.builders = []
+        
+    def getWidget(self):
+        return self.widget
+        
+    def addBuilder(self, builder):
+        self.builders.append(builder)
+
+        name = gtk.Label(builder.name)
+        name.show()
+        self.nameBox.add(name)
+
+        last = gtk.Label('??')
+        last.set_size_request(64,64)
+        lastbox = gtk.EventBox()
+        lastbox.add(last)
+        lastbox.show_all()
+        self.lastBuildBox.add(lastbox)
+
+        status = gtk.Label('??')
+        status.set_size_request(64,64)
+        statusbox = gtk.EventBox()
+        statusbox.add(status)
+        statusbox.show_all()
+        self.statusBox.add(statusbox)
+
+        builder.haveSomeWidgets([name, last, lastbox, status, statusbox])
+
+    def removeBuilder(self, name, builder):
+        self.nameBox.remove(builder.nameSquare)
+        self.lastBuildBox.remove(builder.lastBuildBox)
+        self.statusBox.remove(builder.statusBox)
+        self.builders.remove(builder)
+    
+class CompactBuilder(Builder):
+    def setup(self):
+        self.timer = None
+        self.text = []
+        self.eta = None
+    def start(self):
+        self.nameSquare.set_text(self.name)
+        self.statusSquare.set_text("???")
+        self.subscribe()
+    def haveSomeWidgets(self, widgets):
+        (self.nameSquare,
+         self.lastBuildSquare, self.lastBuildBox,
+         self.statusSquare, self.statusBox) = widgets
+        
+    def remote_currentlyOffline(self):
+        self.eta = None
+        self.stopTimer()
+        self.statusSquare.set_text("offline")
+        self.statusBox.modify_bg(gtk.STATE_NORMAL,
+                                 gtk.gdk.color_parse("red"))
+    def remote_currentlyIdle(self):
+        self.eta = None
+        self.stopTimer()
+        self.statusSquare.set_text("idle")
+    def remote_currentlyWaiting(self, seconds):
+        self.nextBuild = now() + seconds
+        self.startTimer(self.updateWaiting)
+    def remote_currentlyInterlocked(self):
+        self.stopTimer()
+        self.statusSquare.set_text("interlocked")
+    def startTimer(self, func):
+        # the func must clear self.timer and return gtk.FALSE when the event
+        # has arrived
+        self.stopTimer()
+        self.timer = gtk.timeout_add(1000, func)
+        func()
+    def stopTimer(self):
+        if self.timer:
+            gtk.timeout_remove(self.timer)
+            self.timer = None
+    def updateWaiting(self):
+        when = self.nextBuild
+        if now() < when:
+            next = time.strftime("%H:%M:%S", time.localtime(when))
+            secs = "[%d seconds]" % (when - now())
+            self.statusSquare.set_text("waiting\n%s\n%s" % (next, secs))
+            return gtk.TRUE # restart timer
+        else:
+            # done
+            self.statusSquare.set_text("waiting\n[RSN]")
+            self.timer = None
+            return gtk.FALSE
+
+    def remote_currentlyBuilding(self, eta):
+        self.stopTimer()
+        self.statusSquare.set_text("building")
+        if eta:
+            d = eta.callRemote("subscribe", self, 5)
+
+    def remote_newLastBuildStatus(self, event):
+        color = None
+        if event:
+            text = "\n".join(event.text)
+            color = event.color
+        else:
+            text = "none"
+        if not color: color = "gray"
+        self.lastBuildSquare.set_text(text)
+        self.lastBuildBox.modify_bg(gtk.STATE_NORMAL,
+                                    gtk.gdk.color_parse(color))
+
+    def remote_newEvent(self, event):
+        assert(event.__class__ == GtkUpdatingEvent)
+        self.current = event
+        event.builder = self
+        self.text = event.text
+        if not self.text: self.text = ["idle"]
+        self.eta = None
+        self.stopTimer()
+        self.updateText()
+        color = event.color
+        if not color: color = "gray"
+        self.statusBox.modify_bg(gtk.STATE_NORMAL,
+                                 gtk.gdk.color_parse(color))
+
+    def updateCurrent(self):
+        text = self.current.text
+        if text:
+            self.text = text
+            self.updateText()
+        color = self.current.color
+        if color:
+            self.statusBox.modify_bg(gtk.STATE_NORMAL,
+                                     gtk.gdk.color_parse(color))
+    def updateText(self):
+        etatext = []
+        if self.eta:
+            etatext = [time.strftime("%H:%M:%S", time.localtime(self.eta))]
+            if now() > self.eta:
+                etatext += ["RSN"]
+            else:
+                seconds = self.eta - now()
+                etatext += ["[%d secs]" % seconds]
+        text = "\n".join(self.text + etatext)
+        self.statusSquare.set_text(text)
+    def updateTextTimer(self):
+        self.updateText()
+        return gtk.TRUE # restart timer
+    
+    def remote_progress(self, seconds):
+        if seconds == None:
+            self.eta = None
+        else:
+            self.eta = now() + seconds
+        self.startTimer(self.updateTextTimer)
+        self.updateText()
+    def remote_finished(self, eta):
+        self.eta = None
+        self.stopTimer()
+        self.updateText()
+        eta.callRemote("unsubscribe", self)
+'''
+
+class Box:
+    def __init__(self, text="?"):
+        self.text = text
+        self.box = gtk.EventBox()
+        self.label = gtk.Label(text)
+        self.box.add(self.label)
+        self.box.set_size_request(64,64)
+        self.timer = None
+
+    def getBox(self):
+        return self.box
+
+    def setText(self, text):
+        self.text = text
+        self.label.set_text(text)
+
+    def setColor(self, color):
+        if not color:
+            return
+        self.box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
+
+    def setETA(self, eta):
+        if eta:
+            self.when = now() + eta
+            self.startTimer()
+        else:
+            self.stopTimer()
+
+    def startTimer(self):
+        self.stopTimer()
+        self.timer = gobject.timeout_add(1000, self.update)
+        self.update()
+
+    def stopTimer(self):
+        if self.timer:
+            gobject.source_remove(self.timer)
+            self.timer = None
+        self.label.set_text(self.text)
+
+    def update(self):
+        if now() < self.when:
+            next = time.strftime("%H:%M:%S", time.localtime(self.when))
+            secs = "[%d secs]" % (self.when - now())
+            self.label.set_text("%s\n%s\n%s" % (self.text, next, secs))
+            return True # restart timer
+        else:
+            # done
+            self.label.set_text("%s\n[soon]\n[overdue]" % (self.text,))
+            self.timer = None
+            return False
+
+
+
+class ThreeRowBuilder:
+    def __init__(self, name, ref):
+        self.name = name
+
+        self.last = Box()
+        self.current = Box()
+        self.step = Box("idle")
+        self.step.setColor("white")
+
+        self.ref = ref
+
+    def getBoxes(self):
+        return self.last.getBox(), self.current.getBox(), self.step.getBox()
+
+    def getLastBuild(self):
+        d = self.ref.callRemote("getLastFinishedBuild")
+        d.addCallback(self.gotLastBuild)
+    def gotLastBuild(self, build):
+        if build:
+            build.callRemote("getText").addCallback(self.gotLastText)
+            build.callRemote("getColor").addCallback(self.gotLastColor)
+
+    def gotLastText(self, text):
+        self.last.setText("\n".join(text))
+    def gotLastColor(self, color):
+        self.last.setColor(color)
+
+    def getState(self):
+        self.ref.callRemote("getState").addCallback(self.gotState)
+    def gotState(self, res):
+        state, ETA, builds = res
+        # state is one of: offline, idle, waiting, interlocked, building
+        # TODO: ETA is going away, you have to look inside the builds to get
+        # that value
+        currentmap = {"offline": "red",
+                      "idle": "white",
+                      "waiting": "yellow",
+                      "interlocked": "yellow",
+                      "building": "yellow",}
+        text = state
+        self.current.setColor(currentmap[state])
+        if ETA is not None:
+            text += "\nETA=%s secs" % ETA
+        self.current.setText(state)