<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>ChangeLog-0.7.11</filename>
    </added>
    <added>
      <filename>buildbot/process/mtrlogobserver.py</filename>
    </added>
    <added>
      <filename>buildbot/process/subunitlogobserver.py</filename>
    </added>
    <added>
      <filename>buildbot/status/web/console.py</filename>
    </added>
    <added>
      <filename>buildbot/status/web/console_html.py</filename>
    </added>
    <added>
      <filename>buildbot/status/web/console_js.py</filename>
    </added>
    <added>
      <filename>buildbot/test/test_console.py</filename>
    </added>
    <added>
      <filename>contrib/bitbucket_buildbot.py</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -1,6 +1,25 @@
 User visible changes in Buildbot.             -*- outline -*-
 
-* Release 0.7.11 (XXX)
+** Suppression of selected compiler warnings
+
+The WarningCountingShellCommand class has been extended with the ability to
+upload from the slave a file contain warnings to be ignored. See the
+documentation of the suppressionFile argument to the Compile build step.
+
+** New buildstep `MTR'
+
+A new class buildbot.process.mtrlogobserver.MTR was added. This buildstep is
+used to run test suites using mysql-test-run. It parses the stdio output for
+test failures and summarises them on the waterfall page. It also makes server
+error logs available for debugging failures, and optionally inserts
+information about test runs and test failures into an external database.
+
+* Release 0.7.11p (July 16, 2009)
+
+Fixes a few test failures in 0.7.11, and gives a default value for branchType
+if it is not specified by the master.
+
+* Release 0.7.11 (July 5, 2009)
 
 Developers too numerous to mention contributed to this release.  Buildbot has
 truly become a community-maintained application.  Much hard work is not</diff>
      <filename>NEWS</filename>
    </modified>
    <modified>
      <diff>@@ -1 +1 @@
-version = &quot;0.7.11-rc2&quot;
+version = &quot;latest&quot;</diff>
      <filename>buildbot/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -31,6 +31,9 @@ class BuildSet:
     def waitUntilFinished(self):
         return self.status.waitUntilFinished()
 
+    def getProperties(self):
+        return self.properties
+
     def start(self, builders):
         &quot;&quot;&quot;This is called by the BuildMaster to actually create and submit
         the BuildRequests.&quot;&quot;&quot;</diff>
      <filename>buildbot/buildset.py</filename>
    </modified>
    <modified>
      <diff>@@ -205,6 +205,17 @@ class AbstractBuildSlave(NewCredPerspective, service.MultiService):
             return d1
         d.addCallback(_get_info)
 
+        def _get_version(res):
+            d1 = bot.callRemote(&quot;getVersion&quot;)
+            def _got_version(version):
+                state[&quot;version&quot;] = version
+            def _version_unavailable(why):
+                # probably an old slave
+                log.msg(&quot;BuildSlave.version_unavailable&quot;)
+                log.err(why)
+            d1.addCallbacks(_got_version, _version_unavailable)
+        d.addCallback(_get_version)
+
         def _get_commands(res):
             d1 = bot.callRemote(&quot;getCommands&quot;)
             def _got_commands(commands):
@@ -222,6 +233,7 @@ class AbstractBuildSlave(NewCredPerspective, service.MultiService):
         def _accept_slave(res):
             self.slave_status.setAdmin(state.get(&quot;admin&quot;))
             self.slave_status.setHost(state.get(&quot;host&quot;))
+            self.slave_status.setVersion(state.get(&quot;version&quot;))
             self.slave_status.setConnected(True)
             self.slave_commands = state.get(&quot;slave_commands&quot;)
             self.slave = bot
@@ -309,7 +321,7 @@ class AbstractBuildSlave(NewCredPerspective, service.MultiService):
 
     def sendBuilderList(self):
         our_builders = self.botmaster.getBuildersForSlave(self.slavename)
-        blist = [(b.name, b.builddir) for b in our_builders]
+        blist = [(b.name, b.slavebuilddir) for b in our_builders]
         d = self.slave.callRemote(&quot;setBuilderList&quot;, blist)
         return d
 </diff>
      <filename>buildbot/buildslave.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,3 @@
-
 import sys, os, time
 from cPickle import dump
 
@@ -9,6 +8,7 @@ from twisted.application import service
 from twisted.web import html
 
 from buildbot import interfaces, util
+from buildbot.process.properties import Properties
 
 html_tmpl = &quot;&quot;&quot;
 &lt;p&gt;Changed by: &lt;b&gt;%(who)s&lt;/b&gt;&lt;br /&gt;
@@ -22,6 +22,9 @@ Changed files:
 
 Comments:
 %(comments)s
+
+Properties:
+%(properties)s
 &lt;/p&gt;
 &quot;&quot;&quot;
 
@@ -55,7 +58,7 @@ class Change:
 
     def __init__(self, who, files, comments, isdir=0, links=None,
                  revision=None, when=None, branch=None, category=None,
-                 revlink=''):
+                 revlink='', properties={}):
         self.who = who
         self.comments = comments
         self.isdir = isdir
@@ -69,17 +72,26 @@ class Change:
         self.branch = branch
         self.category = category
         self.revlink = revlink
+        self.properties = Properties()
+        self.properties.update(properties, &quot;Change&quot;)
 
         # keep a sorted list of the files, for easier display
         self.files = files[:]
         self.files.sort()
 
+    def __setstate__(self, dict):
+        self.__dict__ = dict
+        # Older Changes won't have a 'properties' attribute in them
+        if not hasattr(self, 'properties'):
+            self.properties = Properties()
+
     def asText(self):
         data = &quot;&quot;
         data += self.getFileContents()
         data += &quot;At: %s\n&quot; % self.getTime()
         data += &quot;Changed By: %s\n&quot; % self.who
-        data += &quot;Comments: %s\n\n&quot; % self.comments
+        data += &quot;Comments: %s&quot; % self.comments
+        data += &quot;Properties: \n%s\n\n&quot; % self.getProperties()
         return data
 
     def asHTML(self):
@@ -91,24 +103,30 @@ class Change:
                 links.append('&lt;a href=&quot;%s&quot;&gt;&lt;b&gt;%s&lt;/b&gt;&lt;/a&gt;' % (link[0], file))
             else:
                 links.append('&lt;b&gt;%s&lt;/b&gt;' % file)
-        revlink = &quot;&quot;
         if self.revision:
             if getattr(self, 'revlink', &quot;&quot;):
                 revision = 'Revision: &lt;a href=&quot;%s&quot;&gt;&lt;b&gt;%s&lt;/b&gt;&lt;/a&gt;\n' % (
                         self.revlink, self.revision)
             else:
                 revision = &quot;Revision: &lt;b&gt;%s&lt;/b&gt;&lt;br /&gt;\n&quot; % self.revision
+        else:
+            revision = ''
 
         branch = &quot;&quot;
         if self.branch:
             branch = &quot;Branch: &lt;b&gt;%s&lt;/b&gt;&lt;br /&gt;\n&quot; % 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) }
+        properties = []
+        for prop in self.properties.asList():
+            properties.append(&quot;%s: %s&lt;br /&gt;&quot; % (prop[0], prop[1]))
+
+        kwargs = { 'who'       : html.escape(self.who),
+                   'at'        : self.getTime(),
+                   'files'     : html.UL(links) + '\n',
+                   'revision'  : revision,
+                   'branch'    : branch,
+                   'comments'  : html.PRE(self.comments),
+                   'properties': html.UL(properties) + '\n' }
         return html_tmpl % kwargs
 
     def get_HTML_box(self, url):
@@ -161,6 +179,12 @@ class Change:
                 data += &quot; %s\n&quot; % f
         return data
         
+    def getProperties(self):
+        data = &quot;&quot;
+        for prop in self.properties.asList():
+            data += &quot;  %s: %s&quot; % (prop[0], prop[1])
+        return data
+
 class ChangeMaster(service.MultiService):
 
     &quot;&quot;&quot;This is the master-side service which receives file change</diff>
      <filename>buildbot/changes/changes.py</filename>
    </modified>
    <modified>
      <diff>@@ -34,8 +34,10 @@ class ChangePerspective(NewCredPerspective):
                                     changedict['comments'],
                                     branch=changedict.get('branch'),
                                     revision=changedict.get('revision'),
+                                    revlink=changedict.get('revlink', ''),
                                     category=changedict.get('category'),
                                     when=changedict.get('when'),
+                                    properties=changedict.get('properties', {})
                                     )
             self.changemaster.addChange(change)
 </diff>
      <filename>buildbot/changes/pb.py</filename>
    </modified>
    <modified>
      <diff>@@ -48,7 +48,7 @@ class SVNPoller(base.ChangeSource, util.ComparableMixin):
     compare_attrs = [&quot;svnurl&quot;, &quot;split_file_function&quot;,
                      &quot;svnuser&quot;, &quot;svnpasswd&quot;,
                      &quot;pollinterval&quot;, &quot;histmax&quot;,
-                     &quot;svnbin&quot;]
+                     &quot;svnbin&quot;, &quot;category&quot;]
 
     parent = None # filled in when we're added
     last_change = None
@@ -58,7 +58,7 @@ class SVNPoller(base.ChangeSource, util.ComparableMixin):
     def __init__(self, svnurl, split_file=None,
                  svnuser=None, svnpasswd=None,
                  pollinterval=10*60, histmax=100,
-                 svnbin='svn', revlinktmpl=''):
+                 svnbin='svn', revlinktmpl='', category=None):
         &quot;&quot;&quot;
         @type  svnurl: string
         @param svnurl: the SVN URL that describes the repository and
@@ -171,6 +171,11 @@ class SVNPoller(base.ChangeSource, util.ComparableMixin):
                              &quot;http://reposerver/websvn/revision.php?rev=%s&quot;
                              would create suitable links on the build pages
                              to information in websvn on each revision.
+
+        @type  category:     string
+        @param category:     A single category associated with the changes that
+                             could be used by schedulers watch for branches of a
+                             certain name AND category.
         &quot;&quot;&quot;
 
         if svnurl.endswith(&quot;/&quot;):
@@ -188,6 +193,7 @@ class SVNPoller(base.ChangeSource, util.ComparableMixin):
         self._prefix = None
         self.overrun_counter = 0
         self.loop = LoopingCall(self.checksvn)
+        self.category = category
 
     def split_file(self, path):
         # use getattr() to avoid turning this function into a bound method,
@@ -457,7 +463,8 @@ class SVNPoller(base.ChangeSource, util.ComparableMixin):
                                comments=comments,
                                revision=revision,
                                branch=branch,
-                               revlink=revlink)
+                               revlink=revlink,
+                               category=self.category)
                     changes.append(c)
 
         return changes</diff>
      <filename>buildbot/changes/svnpoller.py</filename>
    </modified>
    <modified>
      <diff>@@ -10,12 +10,13 @@ class Sender:
         self.port = int(self.port)
         self.num_changes = 0
 
-    def send(self, branch, revision, comments, files, user=None, category=None, when=None):
+    def send(self, branch, revision, comments, files, user=None, category=None,
+             when=None, properties={}):
         if user is None:
             user = self.user
         change = {'who': user, 'files': files, 'comments': comments,
                   'branch': branch, 'revision': revision, 'category': category,
-                  'when': when}
+                  'when': when, 'properties': properties}
         self.num_changes += 1
 
         f = pb.PBClientFactory()</diff>
      <filename>buildbot/clients/sendchange.py</filename>
    </modified>
    <modified>
      <diff>@@ -6,6 +6,7 @@ try:
     import signal
 except ImportError:
     pass
+import string
 from cPickle import load
 import warnings
 
@@ -320,18 +321,12 @@ class DebugPerspective(NewCredPerspective):
     def perspective_print(self, msg):
         print &quot;debug&quot;, msg
 
-class Dispatcher(styles.Versioned):
+class Dispatcher:
     implements(portal.IRealm)
-    persistenceVersion = 2
 
     def __init__(self):
         self.names = {}
 
-    def upgradeToVersion1(self):
-        self.master = self.botmaster.parent
-    def upgradeToVersion2(self):
-        self.names = {}
-
     def register(self, name, afactory):
         self.names[name] = afactory
     def unregister(self, name):
@@ -377,9 +372,8 @@ class Dispatcher(styles.Versioned):
 #   UNIXServer(ResourcePublisher(self.site))
 
 
-class BuildMaster(service.MultiService, styles.Versioned):
+class BuildMaster(service.MultiService):
     debug = 0
-    persistenceVersion = 3
     manhole = None
     debugPassword = None
     projectName = &quot;(unspecified)&quot;
@@ -426,23 +420,6 @@ class BuildMaster(service.MultiService, styles.Versioned):
 
         self.readConfig = False
 
-    def upgradeToVersion1(self):
-        self.dispatcher = self.slaveFactory.root.portal.realm
-
-    def upgradeToVersion2(self): # post-0.4.3
-        self.webServer = self.webTCPPort
-        del self.webTCPPort
-        self.webDistribServer = self.webUNIXPort
-        del self.webUNIXPort
-        self.configFileName = &quot;master.cfg&quot;
-
-    def upgradeToVersion3(self):
-        # post 0.6.3, solely to deal with the 0.6.3 breakage. Starting with
-        # 0.6.5 I intend to do away with .tap files altogether
-        self.services = []
-        self.namedServices = {}
-        del self.change_svc
-
     def startService(self):
         service.MultiService.startService(self)
         self.loadChanges() # must be done before loading the config file
@@ -545,7 +522,7 @@ class BuildMaster(service.MultiService, styles.Versioned):
                       &quot;manhole&quot;, &quot;status&quot;, &quot;projectName&quot;, &quot;projectURL&quot;,
                       &quot;buildbotURL&quot;, &quot;properties&quot;, &quot;prioritizeBuilders&quot;,
                       &quot;eventHorizon&quot;, &quot;buildCacheSize&quot;, &quot;logHorizon&quot;, &quot;buildHorizon&quot;,
-                      &quot;changeHorizon&quot;,
+                      &quot;changeHorizon&quot;, &quot;logMaxSize&quot;, &quot;logMaxTailSize&quot;,
                       )
         for k in config.keys():
             if k not in known_keys:
@@ -578,10 +555,18 @@ class BuildMaster(service.MultiService, styles.Versioned):
             eventHorizon = config.get('eventHorizon', None)
             logHorizon = config.get('logHorizon', None)
             buildHorizon = config.get('buildHorizon', None)
-            logCompressionLimit = config.get('logCompressionLimit')
+            logCompressionLimit = config.get('logCompressionLimit', 4*1024)
             if logCompressionLimit is not None and not \
                     isinstance(logCompressionLimit, int):
                 raise ValueError(&quot;logCompressionLimit needs to be bool or int&quot;)
+            logMaxSize = config.get('logMaxSize')
+            if logMaxSize is not None and not \
+                    isinstance(logMaxSize, int):
+                raise ValueError(&quot;logMaxSize needs to be None or int&quot;)
+            logMaxTailSize = config.get('logMaxTailSize')
+            if logMaxTailSize is not None and not \
+                    isinstance(logMaxTailSize, int):
+                raise ValueError(&quot;logMaxTailSize needs to be None or int&quot;)
             mergeRequests = config.get('mergeRequests')
             if mergeRequests is not None and not callable(mergeRequests):
                 raise ValueError(&quot;mergeRequests must be a callable&quot;)
@@ -658,6 +643,8 @@ class BuildMaster(service.MultiService, styles.Versioned):
         slavenames = [s.slavename for s in slaves]
         buildernames = []
         dirnames = []
+        badchars_map = string.maketrans(&quot;\t !#$%&amp;'()*+,./:;&lt;=&gt;?@[\\]^{|}~&quot;,
+                                        &quot;______________________________&quot;)
         for b in builders:
             if type(b) is tuple:
                 raise ValueError(&quot;builder %s must be defined with a dict, &quot;
@@ -673,6 +660,10 @@ class BuildMaster(service.MultiService, styles.Versioned):
                 raise ValueError(&quot;duplicate builder name %s&quot;
                                  % b['name'])
             buildernames.append(b['name'])
+
+            # Fix the dictionnary with default values.
+            b.setdefault('builddir', b['name'].translate(badchars_map))
+            b.setdefault('slavebuilddir', b['builddir'])
             if b['builddir'] in dirnames:
                 raise ValueError(&quot;builder %s reuses builddir %s&quot;
                                  % (b['name'], b['builddir']))
@@ -750,8 +741,18 @@ class BuildMaster(service.MultiService, styles.Versioned):
 
         self.properties = Properties()
         self.properties.update(properties, self.configFileName)
-        if logCompressionLimit is not None:
-            self.status.logCompressionLimit = logCompressionLimit
+
+        self.status.logCompressionLimit = logCompressionLimit
+        self.status.logMaxSize = logMaxSize
+        self.status.logMaxTailSize = logMaxTailSize
+        # Update any of our existing builders with the current log parameters.
+        # This is required so that the new value is picked up after a
+        # reconfig.
+        for builder in self.botmaster.builders.values():
+            builder.builder_status.setLogCompressionLimit(logCompressionLimit)
+            builder.builder_status.setLogMaxSize(logMaxSize)
+            builder.builder_status.setLogMaxTailSize(logMaxTailSize)
+
         if mergeRequests is not None:
             self.botmaster.mergeRequests = mergeRequests
         if prioritizeBuilders is not None:
@@ -898,7 +899,7 @@ class BuildMaster(service.MultiService, styles.Versioned):
         # everything in newList is either unchanged, changed, or new
         for name, data in newList.items():
             old = self.botmaster.builders.get(name)
-            basedir = data['builddir'] # used on both master and slave
+            basedir = data['builddir']
             #name, slave, builddir, factory = data
             if not old: # new
                 # category added after 0.6.2</diff>
      <filename>buildbot/master.py</filename>
    </modified>
    <modified>
      <diff>@@ -72,13 +72,6 @@ class ReconnectingPBClientFactory(PBClientFactory,
             self.doGetPerspective(self._root)
         self.gotRootObject(self._root)
 
-    def __getstate__(self):
-        # this should get folded into ReconnectingClientFactory
-        d = self.__dict__.copy()
-        d['connector'] = None
-        d['_callID'] = None
-        return d
-
     # oldcred methods
 
     def getPerspective(self, *args):</diff>
      <filename>buildbot/pbutil.py</filename>
    </modified>
    <modified>
      <diff>@@ -240,12 +240,6 @@ class Build:
     def __repr__(self):
         return &quot;&lt;Build %s&gt;&quot; % (self.builder.name,)
 
-    def __getstate__(self):
-        d = self.__dict__.copy()
-        if d.has_key('remote'):
-            del d['remote']
-        return d
-
     def blamelist(self):
         blamelist = []
         for c in self.allChanges():
@@ -291,6 +285,10 @@ class Build:
         for rq in self.requests:
             props.updateFromProperties(rq.properties)
 
+        # and finally, from the SourceStamp, which has properties via Change
+        for change in self.source.changes:
+            props.updateFromProperties(change.properties)
+
         # now set some properties of our own, corresponding to the
         # build itself
         props.setProperty(&quot;buildername&quot;, self.builder.name, &quot;Build&quot;)</diff>
      <filename>buildbot/process/base.py</filename>
    </modified>
    <modified>
      <diff>@@ -116,6 +116,9 @@ class AbstractSlaveBuilder(pb.Referenceable):
         log.err(why)
         return why
 
+    def prepare(self, builder_status):
+        return defer.succeed(None)
+
     def ping(self, timeout, status=None):
         &quot;&quot;&quot;Ping the slave to make sure it is still there. Returns a Deferred
         that fires with True if it is.
@@ -260,7 +263,19 @@ class LatentSlaveBuilder(AbstractSlaveBuilder):
         log.msg(&quot;Latent buildslave %s attached to %s&quot; % (slave.slavename,
                                                          self.builder_name))
 
-    def substantiate(self, build):
+    def prepare(self, builder_status):
+        log.msg(&quot;substantiating slave %s&quot; % (self,))
+        d = self.substantiate()
+        def substantiation_failed(f):
+            builder_status.addPointEvent(['removing', 'latent',
+                                          self.slave.slavename])
+            self.slave.disconnect()
+            # TODO: should failover to a new Build
+            return f
+        d.addErrback(substantiation_failed)
+        return d
+
+    def substantiate(self):
         self.state = SUBSTANTIATING
         d = self.slave.substantiate(self)
         if not self.slave.substantiated:
@@ -335,11 +350,6 @@ class Builder(pb.Referenceable):
     I also manage forced builds, progress expectation (ETA) management, and
     some status delivery chores.
 
-    I am persisted in C{BASEDIR/BUILDERNAME/builder}, so I can remember how
-    long a build usually takes to run (in my C{expectations} attribute). This
-    pickle also includes the L{buildbot.status.builder.BuilderStatus} object,
-    which remembers the set of historic builds.
-
     @type buildable: list of L{buildbot.process.base.BuildRequest}
     @ivar buildable: BuildRequests that are ready to build, but which are
                      waiting for a buildslave to be available.
@@ -360,7 +370,7 @@ class Builder(pb.Referenceable):
         @type  setup: dict
         @param setup: builder setup data, as stored in
                       BuildmasterConfig['builders'].  Contains name,
-                      slavename(s), builddir, factory, locks.
+                      slavename(s), builddir, slavebuilddir, factory, locks.
         @type  builder_status: L{buildbot.status.builder.BuilderStatus}
         &quot;&quot;&quot;
         self.name = setup['name']
@@ -370,6 +380,7 @@ class Builder(pb.Referenceable):
         if setup.has_key('slavenames'):
             self.slavenames.extend(setup['slavenames'])
         self.builddir = setup['builddir']
+        self.slavebuilddir = setup['slavebuilddir']
         self.buildFactory = setup['factory']
         self.nextSlave = setup.get('nextSlave')
         if self.nextSlave is not None and not callable(self.nextSlave):
@@ -422,6 +433,9 @@ class Builder(pb.Referenceable):
         if setup['builddir'] != self.builddir:
             diffs.append('builddir changed from %s to %s' \
                          % (self.builddir, setup['builddir']))
+        if setup['slavebuilddir'] != self.slavebuilddir:
+            diffs.append('slavebuilddir changed from %s to %s' \
+                         % (self.slavebuilddir, setup['slavebuilddir']))
         if setup['factory'] != self.buildFactory: # compare objects
             diffs.append('factory changed')
         oldlocks = [(lock.__class__, lock.name)
@@ -462,18 +476,6 @@ class Builder(pb.Referenceable):
             return True
         return False
 
-    def __getstate__(self):
-        d = self.__dict__.copy()
-        # TODO: note that d['buildable'] can contain Deferreds
-        del d['building'] # TODO: move these back to .buildable?
-        del d['slaves']
-        return d
-
-    def __setstate__(self, d):
-        self.__dict__ = d
-        self.building = []
-        self.slaves = []
-
     def consumeTheSoulOfYourPredecessor(self, old):
         &quot;&quot;&quot;Suck the brain out of an old Builder.
 
@@ -758,27 +760,17 @@ class Builder(pb.Referenceable):
 
         self.building.append(build)
         self.updateBigStatus()
-        if isinstance(sb, LatentSlaveBuilder):
-            log.msg(&quot;starting build %s.. substantiating the slave %s&quot; %
-                    (build, sb))
-            d = sb.substantiate(build)
-            def substantiated(res):
-                return sb.ping(self.START_BUILD_TIMEOUT)
-            def substantiation_failed(res):
-                self.builder_status.addPointEvent(
-                    ['removing', 'latent', sb.slave.slavename])
-                sb.slave.disconnect()
-                # TODO: should failover to a new Build
-                #self.retryBuild(sb.build)
-            d.addCallbacks(substantiated, substantiation_failed)
-        else:
+        log.msg(&quot;starting build %s using slave %s&quot; % (build, sb))
+        d = sb.prepare(self.builder_status)
+        def _ping(ign):
+            # ping the slave to make sure they're still there. If they're
+            # fallen off the map (due to a NAT timeout or something), this
+            # will fail in a couple of minutes, depending upon the TCP
+            # timeout. TODO: consider making this time out faster, or at
+            # least characterize the likely duration.
             log.msg(&quot;starting build %s.. pinging the slave %s&quot; % (build, sb))
-            d = sb.ping(self.START_BUILD_TIMEOUT)
-        # ping the slave to make sure they're still there. If they're fallen
-        # off the map (due to a NAT timeout or something), this will fail in
-        # a couple of minutes, depending upon the TCP timeout. TODO: consider
-        # making this time out faster, or at least characterize the likely
-        # duration.
+            return sb.ping(self.START_BUILD_TIMEOUT)
+        d.addCallback(_ping)
         d.addCallback(self._startBuild_1, build, sb)
         return d
 
@@ -882,7 +874,8 @@ class BuilderControl(components.Adapter):
             return
 
         ss = bs.getSourceStamp(absolute=True)
-        req = base.BuildRequest(reason, ss, self.original.name)
+        req = base.BuildRequest(reason, ss, self.original.name,
+                                properties=bs.getProperties())
         self.requestBuild(req)
 
     def getPendingBuilds(self):
@@ -930,5 +923,3 @@ class BuildRequestControl:
 
     def cancel(self):
         self.original_builder.cancelBuildRequest(self.original_request)
-
-</diff>
      <filename>buildbot/process/builder.py</filename>
    </modified>
    <modified>
      <diff>@@ -62,14 +62,6 @@ class RemoteCommand(pb.Referenceable):
         self.remote_command = remote_command
         self.args = args
 
-    def __getstate__(self):
-        dict = self.__dict__.copy()
-        # Remove the remote ref: if necessary (only for resumed builds), it
-        # will be reattached at resume time
-        if dict.has_key(&quot;remote&quot;):
-            del dict[&quot;remote&quot;]
-        return dict
-
     def run(self, step, remote):
         self.active = True
         self.step = step
@@ -103,12 +95,20 @@ class RemoteCommand(pb.Referenceable):
         @returns: a deferred that will fire when the remote command is
                   done (with None as the result)
         &quot;&quot;&quot;
+
+        # Allow use of WithProperties in logfile path names.
+        cmd_args = self.args
+        if cmd_args.has_key(&quot;logfiles&quot;) and cmd_args[&quot;logfiles&quot;]:
+            cmd_args = cmd_args.copy()
+            properties = self.step.build.getProperties()
+            cmd_args[&quot;logfiles&quot;] = properties.render(cmd_args[&quot;logfiles&quot;])
+
         # This method only initiates the remote command.
         # We will receive remote_update messages as the command runs.
         # We will get a single remote_complete when it finishes.
         # We should fire self.deferred when the command is done.
         d = self.remote.callRemote(&quot;startCommand&quot;, self, self.commandID,
-                                   self.remote_command, self.args)
+                                   self.remote_command, cmd_args)
         return d
 
     def interrupt(self, why):
@@ -256,6 +256,7 @@ class LoggedRemoteCommand(RemoteCommand):
 
     def __init__(self, *args, **kwargs):
         self.logs = {}
+        self.delayedLogs = {}
         self._closeWhenFinished = {}
         RemoteCommand.__init__(self, *args, **kwargs)
 
@@ -290,9 +291,15 @@ class LoggedRemoteCommand(RemoteCommand):
         if not logfileName:
             logfileName = loog.getName()
         assert logfileName not in self.logs
+        assert logfileName not in self.delayedLogs
         self.logs[logfileName] = loog
         self._closeWhenFinished[logfileName] = closeWhenFinished
 
+    def useLogDelayed(self, logfileName, activateCallBack, closeWhenFinished=False):
+        assert logfileName not in self.logs
+        assert logfileName not in self.delayedLogs
+        self.delayedLogs[logfileName] = (activateCallBack, closeWhenFinished)
+
     def start(self):
         log.msg(&quot;LoggedRemoteCommand.start&quot;)
         if 'stdio' not in self.logs:
@@ -313,6 +320,14 @@ class LoggedRemoteCommand(RemoteCommand):
             self.logs['stdio'].addHeader(data)
 
     def addToLog(self, logname, data):
+        # Activate delayed logs on first data.
+        if logname in self.delayedLogs:
+            (activateCallBack, closeWhenFinished) = self.delayedLogs[logname]
+            del self.delayedLogs[logname]
+            loog = activateCallBack(self)
+            self.logs[logname] = loog
+            self._closeWhenFinished[logname] = closeWhenFinished
+
         if logname in self.logs:
             self.logs[logname].addStdout(data)
         else:
@@ -431,10 +446,10 @@ class RemoteShellCommand(LoggedRemoteCommand):
     command is finished, it will fire a Deferred. You can then check the
     results of the command and parse the output however you like.&quot;&quot;&quot;
 
-    def __init__(self, workdir, command, env=None, 
+    def __init__(self, workdir, command, env=None,
                  want_stdout=1, want_stderr=1,
-                 timeout=20*60, logfiles={}, usePTY=&quot;slave-config&quot;,
-                 logEnviron=True):
+                 timeout=20*60, maxTime=None, logfiles={},
+                 usePTY=&quot;slave-config&quot;, logEnviron=True):
         &quot;&quot;&quot;
         @type  workdir: string
         @param workdir: directory where the command ought to run,
@@ -475,6 +490,11 @@ class RemoteShellCommand(LoggedRemoteCommand):
                         None to disable the timeout.
 
         @param logEnviron: whether to log env vars on the slave side
+
+        @type  maxTime: int
+        @param maxTime: tell the remote that if the command fails to complete
+                        in this number of seconds, the command should be
+                        killed.  Use None to disable maxTime.
         &quot;&quot;&quot;
 
         self.command = command # stash .command, set it later
@@ -489,6 +509,7 @@ class RemoteShellCommand(LoggedRemoteCommand):
                 'want_stderr': want_stderr,
                 'logfiles': logfiles,
                 'timeout': timeout,
+                'maxTime': maxTime,
                 'usePTY': usePTY,
                 'logEnviron': logEnviron,
                 }
@@ -958,20 +979,29 @@ class LoggingBuildStep(BuildStep):
     progressMetrics = ('output',)
     logfiles = {}
 
-    parms = BuildStep.parms + ['logfiles']
+    parms = BuildStep.parms + ['logfiles', 'lazylogfiles']
 
-    def __init__(self, logfiles={}, *args, **kwargs):
+    def __init__(self, logfiles={}, lazylogfiles=False, *args, **kwargs):
         BuildStep.__init__(self, *args, **kwargs)
-        self.addFactoryArguments(logfiles=logfiles)
+        self.addFactoryArguments(logfiles=logfiles,
+                                 lazylogfiles=lazylogfiles)
         # merge a class-level 'logfiles' attribute with one passed in as an
         # argument
         self.logfiles = self.logfiles.copy()
         self.logfiles.update(logfiles)
+        self.lazylogfiles = lazylogfiles
         self.addLogObserver('stdio', OutputProgressObserver(&quot;output&quot;))
 
     def describe(self, done=False):
         raise NotImplementedError(&quot;implement this in a subclass&quot;)
 
+    def addLogFile(self, logname, filename):
+        &quot;&quot;&quot;
+        This allows to add logfiles after construction, but before calling
+        startCommand().
+        &quot;&quot;&quot;
+        self.logfiles[logname] = filename
+
     def startCommand(self, cmd, errorMessages=[]):
         &quot;&quot;&quot;
         @param cmd: a suitable RemoteCommand which will be launched, with
@@ -1009,10 +1039,20 @@ class LoggingBuildStep(BuildStep):
         &quot;&quot;&quot;Set up any additional logfiles= logs.
         &quot;&quot;&quot;
         for logname,remotefilename in logfiles.items():
-            # tell the BuildStepStatus to add a LogFile
-            newlog = self.addLog(logname)
-            # and tell the LoggedRemoteCommand to feed it
-            cmd.useLog(newlog, True)
+            if self.lazylogfiles:
+                # Ask LoggedRemoteCommand to watch a logfile, but only add
+                # it when/if we see any data.
+                #
+                # The dummy default argument local_logname is a work-around for
+                # Python name binding; default values are bound by value, but
+                # captured variables in the body are bound by name.
+                callback = lambda cmd_arg, local_logname=logname: self.addLog(local_logname)
+                cmd.useLogDelayed(logname, callback, True)
+            else:
+                # tell the BuildStepStatus to add a LogFile
+                newlog = self.addLog(logname)
+                # and tell the LoggedRemoteCommand to feed it
+                cmd.useLog(newlog, True)
 
     def interrupt(self, reason):
         # TODO: consider adding an INTERRUPTED or STOPPED status to use
@@ -1114,7 +1154,7 @@ class LoggingBuildStep(BuildStep):
         self.step_status.setText(self.getText(cmd, results))
         self.step_status.setText2(self.maybeGetText2(cmd, results))
 
-# (WithProeprties used to be available in this module)
+# (WithProperties used to be available in this module)
 from buildbot.process.properties import WithProperties
 _hush_pyflakes = [WithProperties]
 del _hush_pyflakes</diff>
      <filename>buildbot/process/buildstep.py</filename>
    </modified>
    <modified>
      <diff>@@ -12,6 +12,19 @@ def s(steptype, **kwargs):
     # specification tuples
     return (steptype, kwargs)
 
+class ArgumentsInTheWrongPlace(Exception):
+    &quot;&quot;&quot;When calling BuildFactory.addStep(stepinstance), addStep() only takes
+    one argument. You passed extra arguments to addStep(), which you probably
+    intended to pass to your BuildStep constructor instead. For example, you
+    should do::
+
+     f.addStep(ShellCommand(command=['echo','stuff'], haltOnFailure=True))
+
+    instead of::
+
+     f.addStep(ShellCommand(command=['echo','stuff']), haltOnFailure=True)
+    &quot;&quot;&quot;
+
 class BuildFactory(util.ComparableMixin):
     &quot;&quot;&quot;
     @cvar  buildClass: class to use when creating builds
@@ -42,6 +55,8 @@ class BuildFactory(util.ComparableMixin):
 
     def addStep(self, step_or_factory, **kwargs):
         if isinstance(step_or_factory, BuildStep):
+            if kwargs:
+                raise ArgumentsInTheWrongPlace()
             s = step_or_factory.getStepFactory()
         else:
             s = (step_or_factory, dict(kwargs))</diff>
      <filename>buildbot/process/factory.py</filename>
    </modified>
    <modified>
      <diff>@@ -77,13 +77,6 @@ class RunUnitTestsJelly(RunUnitTests):
                  ResultTypes.SUCCESS: tests.SUCCESS,
                  }
 
-    def __getstate__(self):
-        #d = RunUnitTests.__getstate__(self)
-        d = self.__dict__.copy()
-        # Banana subclasses are Ephemeral
-        if d.has_key(&quot;decoder&quot;):
-            del d['decoder']
-        return d
     def start(self):
         self.decoder = remote.DecodeReport(self)
         # don't accept anything unpleasant from the (untrusted) build slave</diff>
      <filename>buildbot/process/step_twisted2.py</filename>
    </modified>
    <modified>
      <diff>@@ -134,7 +134,7 @@ class Scheduler(BaseUpstreamScheduler):
             self.fileIsImportant = fileIsImportant
 
         self.importantChanges = []
-        self.unimportantChanges = []
+        self.allChanges = []
         self.nextBuildTime = None
         self.timer = None
         self.categories = categories
@@ -164,13 +164,14 @@ class Scheduler(BaseUpstreamScheduler):
     def addImportantChange(self, change):
         log.msg(&quot;%s: change is important, adding %s&quot; % (self, change))
         self.importantChanges.append(change)
+        self.allChanges.append(change)
         self.nextBuildTime = max(self.nextBuildTime,
                                  change.when + self.treeStableTimer)
         self.setTimer(self.nextBuildTime)
 
     def addUnimportantChange(self, change):
         log.msg(&quot;%s: change is not important, adding %s&quot; % (self, change))
-        self.unimportantChanges.append(change)
+        self.allChanges.append(change)
 
     def setTimer(self, when):
         log.msg(&quot;%s: setting timer to %s&quot; %
@@ -191,9 +192,9 @@ class Scheduler(BaseUpstreamScheduler):
         # clear out our state
         self.timer = None
         self.nextBuildTime = None
-        changes = self.importantChanges + self.unimportantChanges
+        changes = self.allChanges
         self.importantChanges = []
-        self.unimportantChanges = []
+        self.allChanges = []
 
         # create a BuildSet, submit it to the BuildMaster
         bs = buildset.BuildSet(self.builderNames,
@@ -215,10 +216,10 @@ class AnyBranchScheduler(BaseUpstreamScheduler):
     fileIsImportant = None
 
     compare_attrs = ('name', 'branches', 'treeStableTimer', 'builderNames',
-                     'fileIsImportant', 'properties')
+                     'fileIsImportant', 'properties', 'categories')
 
     def __init__(self, name, branches, treeStableTimer, builderNames,
-                 fileIsImportant=None, properties={}):
+                 fileIsImportant=None, properties={}, categories=None):
         &quot;&quot;&quot;
         @param name: the name of this Scheduler
         @param branches: The branch names that the Scheduler should pay
@@ -246,6 +247,7 @@ class AnyBranchScheduler(BaseUpstreamScheduler):
 
         @param properties: properties to apply to all builds started from this 
                            scheduler
+        @param categories: A list of categories of changes to accept 
         &quot;&quot;&quot;
 
         BaseUpstreamScheduler.__init__(self, name, properties)
@@ -266,6 +268,7 @@ class AnyBranchScheduler(BaseUpstreamScheduler):
             assert callable(fileIsImportant)
             self.fileIsImportant = fileIsImportant
         self.schedulers = {} # one per branch
+        self.categories = categories
 
     def __repr__(self):
         return &quot;&lt;AnyBranchScheduler '%s'&gt;&quot; % self.name
@@ -290,6 +293,9 @@ class AnyBranchScheduler(BaseUpstreamScheduler):
         if self.branches is not None and branch not in self.branches:
             log.msg(&quot;%s ignoring off-branch %s&quot; % (self, change))
             return
+        if self.categories is not None and change.category not in self.categories:
+            log.msg(&quot;%s ignoring non-matching categories %s&quot; % (self, change))
+            return
         s = self.schedulers.get(branch)
         if not s:
             if branch:
@@ -487,7 +493,7 @@ class Nightly(BaseUpstreamScheduler):
                        % name)
 
         self.importantChanges   = [] 
-        self.unimportantChanges = [] 
+        self.allChanges = [] 
         self.fileIsImportant    = None 
         if fileIsImportant: 
             assert callable(fileIsImportant) 
@@ -584,7 +590,7 @@ class Nightly(BaseUpstreamScheduler):
 
         if  self.onlyIfChanged:
             if len(self.importantChanges) &gt; 0: 
-                changes = self.importantChanges + self.unimportantChanges 
+                changes = self.allChanges 
                 # And trigger a build 
                 log.msg(&quot;Nightly Scheduler &lt;%s&gt;: triggering build&quot; % self.name) 
                 bs = buildset.BuildSet(self.builderNames,
@@ -594,7 +600,7 @@ class Nightly(BaseUpstreamScheduler):
                 self.submitBuildSet(bs)
                 # Reset the change lists 
                 self.importantChanges = [] 
-                self.unimportantChanges = []
+                self.allChanges = []
             else: 
                 log.msg(&quot;Nightly Scheduler &lt;%s&gt;: skipping build - No important change&quot; % self.name)
         else:
@@ -622,11 +628,12 @@ class Nightly(BaseUpstreamScheduler):
  
     def addImportantChange(self, change):
         log.msg(&quot;Nightly Scheduler &lt;%s&gt;: change %s from %s is important, adding it&quot; % (self.name, change.revision, change.who)) 
+        self.allChanges.append(change) 
         self.importantChanges.append(change) 
  
     def addUnimportantChange(self, change):
         log.msg(&quot;Nightly Scheduler &lt;%s&gt;: change %s from %s is not important, adding it&quot; % (self.name, change.revision, change.who)) 
-        self.unimportantChanges.append(change) 
+        self.allChanges.append(change) 
 
 
 class TryBase(BaseScheduler):</diff>
      <filename>buildbot/scheduler.py</filename>
    </modified>
    <modified>
      <diff>@@ -719,6 +719,10 @@ def statusgui(config):
     c.run()
 
 class SendChangeOptions(usage.Options):
+    def __init__(self):
+        usage.Options.__init__(self)
+        self['properties'] = {}
+
     optParameters = [
         (&quot;master&quot;, &quot;m&quot;, None,
          &quot;Location of the buildmaster's PBListener (host:port)&quot;),
@@ -728,6 +732,8 @@ class SendChangeOptions(usage.Options):
         (&quot;revision&quot;, &quot;r&quot;, None, &quot;Revision specifier (string)&quot;),
         (&quot;revision_number&quot;, &quot;n&quot;, None, &quot;Revision specifier (integer)&quot;),
         (&quot;revision_file&quot;, None, None, &quot;Filename containing revision spec&quot;),
+        (&quot;property&quot;, &quot;p&quot;, None,
+         &quot;A property for the change, in the format: name:value&quot;),
         (&quot;comments&quot;, &quot;m&quot;, None, &quot;log message&quot;),
         (&quot;logfile&quot;, &quot;F&quot;, None,
          &quot;Read the log messages from this file (- for stdin)&quot;),
@@ -737,6 +743,9 @@ class SendChangeOptions(usage.Options):
         return &quot;Usage:    buildbot sendchange [options] filenames..&quot;
     def parseArgs(self, *args):
         self['files'] = args
+    def opt_property(self, property):
+        name,value = property.split(':')
+        self['properties'][name] = value
 
 
 def sendchange(config, runReactor=False):
@@ -750,6 +759,7 @@ def sendchange(config, runReactor=False):
     branch = config.get('branch', opts.get('branch'))
     category = config.get('category', opts.get('category'))
     revision = config.get('revision')
+    properties = config.get('properties', {})
     if config.get('when'):
         when = float(config.get('when'))
     else:
@@ -776,7 +786,8 @@ def sendchange(config, runReactor=False):
     assert master, &quot;you must provide the master location&quot;
 
     s = Sender(master, user)
-    d = s.send(branch, revision, comments, files, category=category, when=when)
+    d = s.send(branch, revision, comments, files, category=category, when=when,
+               properties=properties)
     if runReactor:
         d.addCallbacks(s.printSuccess, s.printFailure)
         d.addBoth(s.stop)
@@ -837,6 +848,12 @@ class TryOptions(usage.Options):
          &quot;Run the trial build on this Builder. Can be used multiple times.&quot;],
         [&quot;properties&quot;, None, None,
          &quot;A set of properties made available in the build environment, format:prop=value,propb=valueb...&quot;],
+
+        [&quot;try-topfile&quot;, None, None,
+         &quot;Name of a file at the top of the tree, used to find the top. Only needed for SVN and CVS.&quot;],
+        [&quot;try-topdir&quot;, None, None,
+         &quot;Path to the top of the working copy. Only needed for SVN and CVS.&quot;],
+
         ]
 
     optFlags = [</diff>
      <filename>buildbot/scripts/runner.py</filename>
    </modified>
    <modified>
      <diff>@@ -156,21 +156,17 @@ class BzrExtractor(SourceStampExtractor):
     patchlevel = 0
     vcexe = &quot;bzr&quot;
     def getBaseRevision(self):
-        d = self.dovc([&quot;version-info&quot;])
+        d = self.dovc([&quot;revision-info&quot;,&quot;-rsubmit:&quot;])
         d.addCallback(self.get_revision_number)
         return d
+
     def get_revision_number(self, out):
-        for line in out.split(&quot;\n&quot;):
-            colon = line.find(&quot;:&quot;)
-            if colon != -1:
-                key, value = line[:colon], line[colon+2:]
-                if key == &quot;revno&quot;:
-                    self.baserev = int(value)
-                    return
-        raise ValueError(&quot;unable to find revno: in bzr output: '%s'&quot; % out)
+        revno, revid= out.split()
+        self.baserev = 'revid:' + revid
+        return
 
     def getPatch(self, res):
-        d = self.dovc([&quot;diff&quot;])
+        d = self.dovc([&quot;diff&quot;,&quot;-r%s..&quot; % self.baserev])
         d.addCallback(self.readPatch, self.patchlevel)
         return d
 
@@ -419,7 +415,7 @@ class Try(pb.Referenceable):
             vc = self.getopt(&quot;vc&quot;, &quot;try_vc&quot;)
             if vc in (&quot;cvs&quot;, &quot;svn&quot;):
                 # we need to find the tree-top
-                topdir = self.getopt(&quot;try_topdir&quot;, &quot;try_topdir&quot;)
+                topdir = self.getopt(&quot;try-topdir&quot;, &quot;try_topdir&quot;)
                 if topdir:
                     treedir = os.path.expanduser(topdir)
                 else:</diff>
      <filename>buildbot/scripts/tryclient.py</filename>
    </modified>
    <modified>
      <diff>@@ -339,6 +339,12 @@ class Bot(pb.Referenceable, service.MultiService):
                 files[f] = open(filename, &quot;r&quot;).read()
         return files
 
+    def remote_getVersion(self):
+        &quot;&quot;&quot;Send our version back to the Master&quot;&quot;&quot;
+        return buildbot.version
+
+
+
 class BotFactory(ReconnectingPBClientFactory):
     # 'keepaliveInterval' serves two purposes. The first is to keep the
     # connection alive: it guarantees that there will be at least some</diff>
      <filename>buildbot/slave/bot.py</filename>
    </modified>
    <modified>
      <diff>@@ -285,9 +285,9 @@ class ShellCommand:
     def __init__(self, builder, command,
                  workdir, environ=None,
                  sendStdout=True, sendStderr=True, sendRC=True,
-                 timeout=None, initialStdin=None, keepStdinOpen=False,
-                 keepStdout=False, keepStderr=False, logEnviron=True,
-                 logfiles={}, usePTY=&quot;slave-config&quot;):
+                 timeout=None, maxTime=None, initialStdin=None,
+                 keepStdinOpen=False, keepStdout=False, keepStderr=False,
+                 logEnviron=True, logfiles={}, usePTY=&quot;slave-config&quot;):
         &quot;&quot;&quot;
 
         @param keepStdout: if True, we keep a copy of all the stdout text
@@ -336,6 +336,8 @@ class ShellCommand:
         self.logEnviron = logEnviron
         self.timeout = timeout
         self.timer = None
+        self.maxTime = maxTime
+        self.maxTimer = None
         self.keepStdout = keepStdout
         self.keepStderr = keepStderr
 
@@ -518,6 +520,9 @@ class ShellCommand:
         if self.timeout:
             self.timer = reactor.callLater(self.timeout, self.doTimeout)
 
+        if self.maxTime:
+            self.maxTimer = reactor.callLater(self.maxTime, self.doMaxTimeout)
+
         for w in self.logFileWatchers:
             w.start()
 
@@ -570,6 +575,9 @@ class ShellCommand:
         if self.timer:
             self.timer.cancel()
             self.timer = None
+        if self.maxTimer:
+            self.maxTimer.cancel()
+            self.maxTimer = None
         d = self.deferred
         self.deferred = None
         if d:
@@ -582,6 +590,9 @@ class ShellCommand:
         if self.timer:
             self.timer.cancel()
             self.timer = None
+        if self.maxTimer:
+            self.maxTimer.cancel()
+            self.maxTimer = None
         d = self.deferred
         self.deferred = None
         if d:
@@ -594,12 +605,20 @@ class ShellCommand:
         msg = &quot;command timed out: %d seconds without output&quot; % self.timeout
         self.kill(msg)
 
+    def doMaxTimeout(self):
+        self.maxTimer = None
+        msg = &quot;command timed out: %d seconds elapsed&quot; % self.maxTime
+        self.kill(msg)
+
     def kill(self, msg):
         # This may be called by the timeout, or when the user has decided to
         # abort this build.
         if self.timer:
             self.timer.cancel()
             self.timer = None
+        if self.maxTimer:
+            self.maxTimer.cancel()
+            self.maxTimer = None
         if hasattr(self.process, &quot;pid&quot;) and self.process.pid is not None:
             msg += &quot;, killing pid %s&quot; % self.process.pid
         log.msg(msg)
@@ -1192,6 +1211,7 @@ class SlaveShellCommand(Command):
                       configuration of the slave)
         - ['not_really']: 1 to skip execution and return rc=0
         - ['timeout']: seconds of silence to tolerate before killing command
+        - ['maxTime']: seconds before killing command
         - ['logfiles']: dict mapping LogFile name to the workdir-relative
                         filename of a local log file. This local file will be
                         watched just like 'tail -f', and all changes will be
@@ -1215,6 +1235,7 @@ class SlaveShellCommand(Command):
         c = ShellCommand(self.builder, args['command'],
                          workdir, environ=args.get('env'),
                          timeout=args.get('timeout', None),
+                         maxTime=args.get('maxTime', None),
                          sendStdout=args.get('want_stdout', True),
                          sendStderr=args.get('want_stderr', True),
                          sendRC=True,
@@ -1357,6 +1378,8 @@ class SourceBase(Command):
         - ['timeout']:  seconds of silence tolerated before we kill off the
                         command
 
+        - ['maxTime']:  seconds before we kill off the command
+
         - ['retry']:    If not None, this is a tuple of (delay, repeats)
                         which means that any failed VC updates should be
                         reattempted, up to REPEATS times, after a delay of
@@ -1378,6 +1401,7 @@ class SourceBase(Command):
         self.revision = args.get('revision')
         self.patch = args.get('patch')
         self.timeout = args.get('timeout', 120)
+        self.maxTime = args.get('maxTime', None)
         self.retry = args.get('retry')
         # VC-specific subclasses should override this to extract more args.
         # Make sure to upcall!
@@ -1564,7 +1588,8 @@ class SourceBase(Command):
             return defer.succeed(0)
         command = [&quot;rm&quot;, &quot;-rf&quot;, d]
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=0, timeout=self.timeout, usePTY=False)
+                         sendRC=0, timeout=self.timeout, maxTime=self.maxTime,
+                         usePTY=False)
 
         self.command = c
         # sendRC=0 means the rm command will send stdout/stderr to the
@@ -1594,22 +1619,41 @@ class SourceBase(Command):
 
         command = ['cp', '-R', '-P', '-p', fromdir, todir]
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout, maxTime=self.maxTime,
+                         usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
         return d
 
     def doPatch(self, res):
-        patchlevel, diff = self.patch
-        command = [getCommand(&quot;patch&quot;), '-p%d' % patchlevel]
+        patchlevel = self.patch[0]
+        diff = self.patch[1]
+        root = None
+        if len(self.patch) &gt;= 3:
+            root = self.patch[2]
+        command = [
+            getCommand(&quot;patch&quot;),
+            '-p%d' % patchlevel,
+            '--remove-empty-files',
+            '--force',
+            '--forward',
+        ]
         dir = os.path.join(self.builder.basedir, self.workdir)
         # mark the directory so we don't try to update it later
         open(os.path.join(dir, &quot;.buildbot-patched&quot;), &quot;w&quot;).write(&quot;patched\n&quot;)
+
+        # Update 'dir' with the 'root' option. Make sure it is a subdirectory
+        # of dir.
+        if (root and
+            os.path.abspath(os.path.join(dir, root)
+                            ).startswith(os.path.abspath(dir))):
+            dir = os.path.join(dir, root)
+
         # now apply the patch
         c = ShellCommand(self.builder, command, dir,
                          sendRC=False, timeout=self.timeout,
-                         initialStdin=diff, usePTY=False)
+                         maxTime=self.maxTime, initialStdin=diff, usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
@@ -1658,6 +1702,7 @@ class CVS(SourceBase):
                        + ['login'])
             c = ShellCommand(self.builder, command, d,
                              sendRC=False, timeout=self.timeout,
+                             maxTime=self.maxTime,
                              initialStdin=self.login+&quot;\n&quot;, usePTY=False)
             self.command = c
             d = c.start()
@@ -1679,7 +1724,8 @@ class CVS(SourceBase):
         if self.revision:
             command += ['-D', self.revision]
         c = ShellCommand(self.builder, command, d,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         return c.start()
 
@@ -1702,7 +1748,8 @@ class CVS(SourceBase):
         command += [self.cvsmodule]
         
         c = ShellCommand(self.builder, command, d,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         return c.start()
 
@@ -1757,7 +1804,7 @@ class SVN(SourceBase):
                    '--non-interactive', '--no-auth-cache']
         c = ShellCommand(self.builder, command, d,
                          sendRC=False, timeout=self.timeout,
-                         keepStdout=True, usePTY=False)
+                         maxTime=self.maxTime, keepStdout=True, usePTY=False)
         self.command = c
         return c.start()
 
@@ -1779,7 +1826,7 @@ class SVN(SourceBase):
                         self.svnurl, self.srcdir]
         c = ShellCommand(self.builder, command, d,
                          sendRC=False, timeout=self.timeout,
-                         keepStdout=True, usePTY=False)
+                         maxTime=self.maxTime, keepStdout=True, usePTY=False)
         self.command = c
         return c.start()
 
@@ -1858,7 +1905,8 @@ class Darcs(SourceBase):
         d = os.path.join(self.builder.basedir, self.srcdir)
         command = [self.vcexe, 'pull', '--all', '--verbose']
         c = ShellCommand(self.builder, command, d,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         return c.start()
 
@@ -1879,7 +1927,8 @@ class Darcs(SourceBase):
         command.append(self.repourl)
 
         c = ShellCommand(self.builder, command, d,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         d = c.start()
         if self.revision:
@@ -1951,7 +2000,8 @@ class Monotone(SourceBase):
                    &quot;-r&quot;, self.revision,
                    &quot;-b&quot;, self.branch]
         c = ShellCommand(self.builder, command, self.full_srcdir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         return c.start()
 
@@ -1965,7 +2015,8 @@ class Monotone(SourceBase):
                    &quot;-b&quot;, self.branch,
                    self.full_srcdir]
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         return c.start()
 
@@ -1986,7 +2037,8 @@ class Monotone(SourceBase):
             command = [self.monotone, &quot;db&quot;, &quot;init&quot;,
                        &quot;--db=&quot; + self.full_db_path]
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
@@ -1998,7 +2050,8 @@ class Monotone(SourceBase):
         command = [self.monotone, &quot;--db=&quot; + self.full_db_path,
                    &quot;pull&quot;, &quot;--ticker=dot&quot;, self.server_addr, self.branch]
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, timeout=self._pull_timeout, usePTY=False)
+                         sendRC=False, timeout=self._pull_timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.sendStatus({&quot;header&quot;: &quot;pulling %s from %s\n&quot;
                                    % (self.branch, self.server_addr)})
         self.command = c
@@ -2025,6 +2078,7 @@ class Git(SourceBase):
 
     def setup(self, args):
         SourceBase.setup(self, args)
+        self.vcexe = getCommand(&quot;git&quot;)
         self.repourl = args['repourl']
         self.branch = args.get('branch')
         if not self.branch:
@@ -2046,6 +2100,17 @@ class Git(SourceBase):
     def readSourcedata(self):
         return open(self.sourcedatafile, &quot;r&quot;).read()
 
+    def _dovccmd(self, command, cb=None, **kwargs):
+        c = ShellCommand(self.builder, [self.vcexe] + command, self._fullSrcdir(),
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False, **kwargs)
+        self.command = c
+        d = c.start()
+        if cb:
+            d.addCallback(self._abandonOnFailure)
+            d.addCallback(cb)
+        return d
+
     # If the repourl matches the sourcedata file, then
     # we can say that the sourcedata matches.  We can
     # ignore branch changes, since Git can work with
@@ -2060,12 +2125,17 @@ class Git(SourceBase):
             return False
         return True
 
-    def _didSubmodules(self, res):
-        command = ['git', 'submodule', 'update', '--init']
-        c = ShellCommand(self.builder, command, self._fullSrcdir(),
-                         sendRC=False, timeout=self.timeout, usePTY=False)
-        self.command = c
-        return c.start()
+    def _cleanSubmodules(self, res):
+        return self._dovccmd(['submodule', 'foreach', 'git', 'clean', '-dfx'])
+
+    def _updateSubmodules(self, res):
+        return self._dovccmd(['submodule', 'update'], self._cleanSubmodules)
+
+    def _initSubmodules(self, res):
+        if self.submodules:
+            return self._dovccmd(['submodule', 'init'], self._updateSubmodules)
+        else:
+            return defer.succeed(0)
 
     def _didFetch(self, res):
         if self.revision:
@@ -2073,66 +2143,51 @@ class Git(SourceBase):
         else:
             head = 'FETCH_HEAD'
 
-        command = ['git', 'reset', '--hard', head]
-        c = ShellCommand(self.builder, command, self._fullSrcdir(),
-                         sendRC=False, timeout=self.timeout, usePTY=False)
-        self.command = c
-        d = c.start()
-        if self.submodules:
-            d.addCallback(self._abandonOnFailure)
-            d.addCallback(self._didSubmodules)
-        return d
+        command = ['reset', '--hard', head]
+        return self._dovccmd(command, self._initSubmodules)
 
     # Update first runs &quot;git clean&quot;, removing local changes, This,
     # combined with the later &quot;git reset&quot; equates clobbering the repo,
     # but it's much more efficient.
     def doVCUpdate(self):
-        command = ['git', 'clean', '-f', '-d', '-x']
-        c = ShellCommand(self.builder, command, self._fullSrcdir(),
-                         sendRC=False, timeout=self.timeout, usePTY=False)
-        self.command = c
-        d = c.start()
-        d.addCallback(self._abandonOnFailure)
-        d.addCallback(self._didClean)
-        return d
+        command = ['clean', '-f', '-d', '-x']
+        return self._dovccmd(command, self._didClean)
 
-    def _didClean(self, dummy):
-        command = ['git', 'fetch', '-t', self.repourl, self.branch]
+    def _doFetch(self, dummy):
+        command = ['fetch', '-t', self.repourl, self.branch]
         self.sendStatus({&quot;header&quot;: &quot;fetching branch %s from %s\n&quot;
                                         % (self.branch, self.repourl)})
-        c = ShellCommand(self.builder, command, self._fullSrcdir(),
-                         sendRC=False, timeout=self.timeout, usePTY=False)
-        self.command = c
-        d = c.start()
-        d.addCallback(self._abandonOnFailure)
-        d.addCallback(self._didFetch)
-        return d
+        return self._dovccmd(command, self._didFetch)
+
+    def _didClean(self, dummy):
+        # After a clean, try to use the given revision if we have one.
+        if self.revision:
+            # We know what revision we want.  See if we have it.
+            d = self._dovccmd(['reset', '--hard', self.revision],
+                              self._initSubmodules)
+            # If we are unable to reset to the specified version, we
+            # must do a fetch first and retry.
+            d.addErrback(self._doFetch)
+            return d
+        else:
+            # No known revision, go grab the latest.
+            return self._doFetch(None)
 
     def _didInit(self, res):
         return self.doVCUpdate()
 
     def doVCFull(self):
-        os.mkdir(self._fullSrcdir())
-        c = ShellCommand(self.builder, ['git', 'init'], self._fullSrcdir(),
-                         sendRC=False, timeout=self.timeout, usePTY=False)
-        self.command = c
-        d = c.start()
-        d.addCallback(self._abandonOnFailure)
-        d.addCallback(self._didInit)
-        return d
+        os.makedirs(self._fullSrcdir())
+        return self._dovccmd(['init'], self._didInit)
 
     def parseGotRevision(self):
-        command = ['git', 'rev-parse', 'HEAD']
-        c = ShellCommand(self.builder, command, self._fullSrcdir(),
-                         sendRC=False, keepStdout=True, usePTY=False)
-        d = c.start()
+        command = ['rev-parse', 'HEAD']
         def _parse(res):
-            hash = c.stdout.strip()
+            hash = self.command.stdout.strip()
             if len(hash) != 40:
                 return None
             return hash
-        d.addCallback(_parse)
-        return d
+        return self._dovccmd(command, _parse, keepStdout=True)
 
 registerSlaveCommand(&quot;git&quot;, Git, command_version)
 
@@ -2182,7 +2237,8 @@ class Arch(SourceBase):
         if self.revision:
             command.append(self.revision)
         c = ShellCommand(self.builder, command, d,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         return c.start()
 
@@ -2194,8 +2250,8 @@ class Arch(SourceBase):
 
         command = [self.vcexe, 'register-archive', '--force', self.url]
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, keepStdout=True,
-                         timeout=self.timeout, usePTY=False)
+                         sendRC=False, keepStdout=True, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
@@ -2228,7 +2284,8 @@ class Arch(SourceBase):
                    '--no-pristine',
                    ver, self.srcdir]
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
@@ -2240,7 +2297,8 @@ class Arch(SourceBase):
         d = os.path.join(self.builder.basedir, self.srcdir)
         command = [self.vcexe, 'build-config', self.buildconfig]
         c = ShellCommand(self.builder, command, d,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
@@ -2300,7 +2358,8 @@ class Bazaar(Arch):
         command = [self.vcexe, 'get', '--no-pristine',
                    ver, self.srcdir]
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
@@ -2375,7 +2434,8 @@ class Bzr(SourceBase):
         srcdir = os.path.join(self.builder.basedir, self.srcdir)
         command = [self.vcexe, 'update']
         c = ShellCommand(self.builder, command, srcdir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         return c.start()
 
@@ -2403,7 +2463,8 @@ class Bzr(SourceBase):
         command.append(self.srcdir)
 
         c = ShellCommand(self.builder, command, d,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         d = c.start()
         return d
@@ -2418,13 +2479,15 @@ class Bzr(SourceBase):
         command.append(self.repourl)
         command.append(tmpdir)
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         d = c.start()
         def _export(res):
             command = [self.vcexe, 'export', srcdir]
             c = ShellCommand(self.builder, command, tmpdir,
-                             sendRC=False, timeout=self.timeout, usePTY=False)
+                             sendRC=False, timeout=self.timeout,
+                             maxTime=self.maxTime, usePTY=False)
             self.command = c
             return c.start()
         d.addCallback(_export)
@@ -2514,7 +2577,7 @@ class Mercurial(SourceBase):
         command = [self.vcexe, 'pull', '--verbose', self.repourl]
         c = ShellCommand(self.builder, command, d,
                          sendRC=False, timeout=self.timeout,
-                         keepStdout=True, usePTY=False)
+                         maxTime=self.maxTime, keepStdout=True, usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._handleEmptyUpdate)
@@ -2542,7 +2605,8 @@ class Mercurial(SourceBase):
         command.extend([self.repourl, d])
         
         c = ShellCommand(self.builder, command, self.builder.basedir,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         cmd1 = c.start()
         cmd1.addCallback(self._update)
@@ -2707,7 +2771,7 @@ class Mercurial(SourceBase):
             updatecmd.extend(['--rev', self.args.get('branch',  'default')])
         self.command = ShellCommand(self.builder, updatecmd,
             self.builder.basedir, sendRC=False,
-            timeout=self.timeout, usePTY=False)
+            timeout=self.timeout, maxTime=self.maxTime, usePTY=False)
         return self.command.start()
 
     def parseGotRevision(self):
@@ -2758,8 +2822,9 @@ class P4Base(SourceBase):
         command.extend(['changes', '-m', '1', '#have'])
         c = ShellCommand(self.builder, command, self.builder.basedir,
                          environ=self.env, timeout=self.timeout,
-                         sendStdout=True, sendStderr=False, sendRC=False,
-                         keepStdout=True, usePTY=False)
+                         maxTime=self.maxTime, sendStdout=True,
+                         sendStderr=False, sendRC=False, keepStdout=True,
+                         usePTY=False)
         self.command = c
         d = c.start()
 
@@ -2845,7 +2910,7 @@ class P4(P4Base):
         env = {}
         c = ShellCommand(self.builder, command, self.builder.basedir,
                          environ=env, sendRC=False, timeout=self.timeout,
-                         keepStdout=True, usePTY=False)
+                         maxTime=self.maxTime, keepStdout=True, usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
@@ -2882,7 +2947,8 @@ class P4(P4Base):
         log.msg(client_spec)
         c = ShellCommand(self.builder, command, self.builder.basedir,
                          environ=env, sendRC=False, timeout=self.timeout,
-                         initialStdin=client_spec, usePTY=False)
+                         maxTime=self.maxTime, initialStdin=client_spec,
+                         usePTY=False)
         self.command = c
         d = c.start()
         d.addCallback(self._abandonOnFailure)
@@ -2936,7 +3002,8 @@ class P4Sync(P4Base):
             command.extend(['@' + self.revision])
         env = {}
         c = ShellCommand(self.builder, command, d, environ=env,
-                         sendRC=False, timeout=self.timeout, usePTY=False)
+                         sendRC=False, timeout=self.timeout,
+                         maxTime=self.maxTime, usePTY=False)
         self.command = c
         return c.start()
 </diff>
      <filename>buildbot/slave/commands.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,3 @@
-
 from zope.interface import implements
 from buildbot import util, interfaces
 </diff>
      <filename>buildbot/sourcestamp.py</filename>
    </modified>
    <modified>
      <diff>@@ -12,7 +12,11 @@ import os, shutil, sys, re, urllib, itertools
 import gc
 from cPickle import load, dump
 from cStringIO import StringIO
-from bz2 import BZ2File
+
+try: # bz2 is not available on py23
+    from bz2 import BZ2File
+except ImportError:
+    BZ2File = None
 
 # sibling imports
 from buildbot import interfaces, util, sourcestamp
@@ -220,8 +224,15 @@ class LogFile:
 
     finished = False
     length = 0
+    nonHeaderLength = 0
+    tailLength = 0
     chunkSize = 10*1000
     runLength = 0
+    # No max size by default
+    logMaxSize = None
+    # Don't keep a tail buffer by default
+    logMaxTailSize = None
+    maxLengthExceeded = False
     runEntries = [] # provided so old pickled builds will getChunks() ok
     entries = None
     BUFFERSIZE = 2048
@@ -251,6 +262,7 @@ class LogFile:
         self.runEntries = []
         self.watchers = []
         self.finishedWatchers = []
+        self.tailBuffer = []
 
     def getFilename(self):
         return os.path.join(self.step.build.builder.basedir, self.filename)
@@ -282,10 +294,11 @@ class LogFile:
             return self.openfile
         # otherwise they get their own read-only handle
         # try a compressed log first
-        try:
-            return BZ2File(self.getFilename() + &quot;.bz2&quot;, &quot;r&quot;)
-        except IOError:
-            pass
+        if BZ2File is not None:
+            try:
+                return BZ2File(self.getFilename() + &quot;.bz2&quot;, &quot;r&quot;)
+            except IOError:
+                pass
         return open(self.getFilename(), &quot;r&quot;)
 
     def getText(self):
@@ -404,6 +417,31 @@ class LogFile:
 
     def addEntry(self, channel, text):
         assert not self.finished
+
+        if channel != HEADER:
+            # Truncate the log if it's more than logMaxSize bytes
+            if self.logMaxSize and self.nonHeaderLength &gt; self.logMaxSize:
+                # Add a message about what's going on
+                if not self.maxLengthExceeded:
+                    msg = &quot;\nOutput exceeded %i bytes, remaining output has been truncated\n&quot; % self.logMaxSize
+                    self.addEntry(HEADER, msg)
+                    self.merge()
+                    self.maxLengthExceeded = True
+
+                if self.logMaxTailSize:
+                    # Update the tail buffer
+                    self.tailBuffer.append((channel, text))
+                    self.tailLength += len(text)
+                    while self.tailLength &gt; self.logMaxTailSize:
+                        # Drop some stuff off the beginning of the buffer
+                        c,t = self.tailBuffer.pop(0)
+                        n = len(t)
+                        self.tailLength -= n
+                        assert self.tailLength &gt;= 0
+                return
+
+            self.nonHeaderLength += len(text)
+
         # we only add to .runEntries here. merge() is responsible for adding
         # merged chunks to .entries
         if self.runEntries and channel != self.runEntries[0][0]:
@@ -425,7 +463,19 @@ class LogFile:
         self.addEntry(HEADER, text)
 
     def finish(self):
-        self.merge()
+        if self.tailBuffer:
+            msg = &quot;\nFinal %i bytes follow below:\n&quot; % self.tailLength
+            tmp = self.runEntries
+            self.runEntries = [(HEADER, msg)]
+            self.merge()
+            self.runEntries = self.tailBuffer
+            self.merge()
+            self.runEntries = tmp
+            self.merge()
+            self.tailBuffer = []
+        else:
+            self.merge()
+
         if self.openfile:
             # we don't do an explicit close, because there might be readers
             # shareing the filehandle. As soon as they stop reading, the
@@ -444,6 +494,10 @@ class LogFile:
 
 
     def compressLog(self):
+        # bail out if there's no compression support
+        if BZ2File is None:
+            return
+
         compressed = self.getFilename() + &quot;.bz2.tmp&quot;
         d = threads.deferToThread(self._compressLog, compressed)
         d.addCallback(self._renameCompressedLog, compressed)
@@ -885,6 +939,8 @@ class BuildStepStatus(styles.Versioned):
         assert self.started # addLog before stepStarted won't notify watchers
         logfilename = self.build.generateLogfileName(self.name, name)
         log = LogFile(self, name, logfilename)
+        log.logMaxSize = self.build.builder.logMaxSize
+        log.logMaxTailSize = self.build.builder.logMaxTailSize
         self.logs.append(log)
         for w in self.watchers:
             receiver = w.logStarted(self.build, self, log)
@@ -943,7 +999,9 @@ class BuildStepStatus(styles.Versioned):
             if logCompressionLimit is not False and \
                     isinstance(loog, LogFile):
                 if os.path.getsize(loog.getFilename()) &gt; logCompressionLimit:
-                    cld.append(loog.compressLog())
+                    loog_deferred = loog.compressLog()
+                    if loog_deferred:
+                        cld.append(loog_deferred)
 
         for r in self.updates.keys():
             if self.updates[r] is not None:
@@ -1454,6 +1512,8 @@ class BuilderStatus(styles.Versioned):
         self.buildCache = weakref.WeakValueDictionary()
         self.buildCache_LRU = []
         self.logCompressionLimit = False # default to no compression for tests
+        self.logMaxSize = None # No default limit
+        self.logMaxTailSize = None # No tail buffering
 
     # persistence
 
@@ -1526,6 +1586,12 @@ class BuilderStatus(styles.Versioned):
     def setLogCompressionLimit(self, lowerLimit):
         self.logCompressionLimit = lowerLimit
 
+    def setLogMaxSize(self, upperLimit):
+        self.logMaxSize = upperLimit
+
+    def setLogMaxTailSize(self, tailSize):
+        self.logMaxTailSize = tailSize
+
     def saveYourself(self):
         for b in self.currentBuilds:
             if not b.isFinished:
@@ -1726,6 +1792,11 @@ class BuilderStatus(styles.Versioned):
         for Nb in range(1, self.nextBuildNumber+1):
             b = self.getBuild(-Nb)
             if not b:
+                # HACK: If this is the first build we are looking at, it is
+                # possible it's in progress but locked before it has written a
+                # pickle; in this case keep looking.
+                if Nb == 1:
+                    continue
                 break
             if branches and not b.getSourceStamp().branch in branches:
                 continue
@@ -1952,6 +2023,7 @@ class SlaveStatus:
 
     admin = None
     host = None
+    version = None
     connected = False
     graceful_shutdown = False
 
@@ -1967,6 +2039,8 @@ class SlaveStatus:
         return self.admin
     def getHost(self):
         return self.host
+    def getVersion(self):
+        return self.version
     def isConnected(self):
         return self.connected
     def lastMessageReceived(self):
@@ -1978,6 +2052,8 @@ class SlaveStatus:
         self.admin = admin
     def setHost(self, host):
         self.host = host
+    def setVersion(self, version):
+        self.version = version
     def setConnected(self, isConnected):
         self.connected = isConnected
     def setLastMessageReceived(self, when):
@@ -2034,6 +2110,9 @@ class Status:
         assert os.path.isdir(basedir)
         # compress logs bigger than 4k, a good default on linux
         self.logCompressionLimit = 4*1024
+        # No default limit to the log size
+        self.logMaxSize = None
+        self.logMaxTailSize = None
 
 
     # methods called by our clients
@@ -2247,6 +2326,8 @@ class Status:
 
         builder_status.setBigState(&quot;offline&quot;)
         builder_status.setLogCompressionLimit(self.logCompressionLimit)
+        builder_status.setLogMaxSize(self.logMaxSize)
+        builder_status.setLogMaxTailSize(self.logMaxTailSize)
 
         for t in self.watchers:
             self.announceNewBuilder(t, name, builder_status)</diff>
      <filename>buildbot/status/builder.py</filename>
    </modified>
    <modified>
      <diff>@@ -190,7 +190,7 @@ class MailNotifier(base.StatusReceiverMultiService):
                  subject=&quot;buildbot %(result)s in %(projectName)s on %(builder)s&quot;,
                  lookup=None, extraRecipients=[],
                  sendToInterestedUsers=True, customMesg=message,
-                 extraHeaders=None):
+                 extraHeaders=None, addPatch=True):
         &quot;&quot;&quot;
         @type  fromaddr: string
         @param fromaddr: the email address to be used in the 'From' header.
@@ -233,12 +233,16 @@ class MailNotifier(base.StatusReceiverMultiService):
                            categories). Use either builders or categories,
                            but not both.
 
-        @type  addLogs: boolean.
+        @type  addLogs: boolean
         @param addLogs: if True, include all build logs as attachments to the
                         messages.  These can be quite large. This can also be
                         set to a list of log names, to send a subset of the
                         logs. Defaults to False.
 
+        @type  addPatch: boolean
+        @param addPatch: if True, include the patch when the source stamp
+                         includes one.
+
         @type  relayhost: string
         @param relayhost: the host to which the outbound SMTP connection
                           should be made. Defaults to 'localhost'
@@ -340,6 +344,7 @@ class MailNotifier(base.StatusReceiverMultiService):
         if extraHeaders:
             assert isinstance(extraHeaders, dict)
         self.extraHeaders = extraHeaders
+        self.addPatch = addPatch
         self.watched = []
         self.status = None
 
@@ -461,7 +466,7 @@ class MailNotifier(base.StatusReceiverMultiService):
         assert type in ('plain', 'html'), &quot;'%s' message type must be 'plain' or 'html'.&quot; % type
 
         haveAttachments = False
-        if attrs['patch'] or self.addLogs:
+        if (attrs['patch'] and self.addPatch) or self.addLogs:
             haveAttachments = True
             if not canDoAttachments:
                 twlog.msg(&quot;warning: I want to send mail with attachments, &quot;
@@ -485,7 +490,7 @@ class MailNotifier(base.StatusReceiverMultiService):
         m['From'] = self.fromaddr
         # m['To'] is added later
 
-        if attrs['patch']:
+        if attrs['patch'] and self.addPatch:
             a = MIMEText(attrs['patch'][1])
             a.add_header('Content-Disposition', &quot;attachment&quot;,
                          filename=&quot;source patch&quot;)</diff>
      <filename>buildbot/status/mail.py</filename>
    </modified>
    <modified>
      <diff>@@ -84,6 +84,22 @@ def make_stop_form(stopURL, useUserPasswd, on_all=False, label=&quot;Build&quot;):
     data += '&lt;input type=&quot;submit&quot; value=&quot;Stop %s&quot; /&gt;&lt;/form&gt;\n' % label
     return data
 
+def make_extra_property_row(N):
+    &quot;&quot;&quot;helper function to create the html for adding extra build
+    properties to a forced (or resubmitted) build. &quot;N&quot; is an integer
+    inserted into the form names so that more than one property can be
+    used in the form.
+    &quot;&quot;&quot;
+    prop_html = '''
+    &lt;div class=&quot;row&quot;&gt;Property %(N)i
+      &lt;span class=&quot;label&quot;&gt;Name:&lt;/span&gt;
+      &lt;span class=&quot;field&quot;&gt;&lt;input type=&quot;text&quot; name=&quot;property%(N)iname&quot; /&gt;&lt;/span&gt;
+      &lt;span class=&quot;label&quot;&gt;Value:&lt;/span&gt;
+      &lt;span class=&quot;field&quot;&gt;&lt;input type=&quot;text&quot; name=&quot;property%(N)ivalue&quot; /&gt;&lt;/span&gt;
+    &lt;/div&gt;
+    ''' % {&quot;N&quot;: N}
+    return prop_html
+
 def make_force_build_form(forceURL, useUserPasswd, on_all=False):
     if on_all:
         data = &quot;&quot;&quot;&lt;form method=&quot;post&quot; action=&quot;%s&quot; class=&quot;command forcebuild&quot;&gt;
@@ -101,6 +117,9 @@ def make_force_build_form(forceURL, useUserPasswd, on_all=False):
                  &quot;&lt;input type='text' name='branch' /&gt;&quot;)
       + make_row(&quot;Revision to build:&quot;,
                  &quot;&lt;input type='text' name='revision' /&gt;&quot;)
+      + make_extra_property_row(1)
+      + make_extra_property_row(2)
+      + make_extra_property_row(3)
       + '&lt;input type=&quot;submit&quot; value=&quot;Force Build&quot; /&gt;&lt;/form&gt;\n')
 
 def td(text=&quot;&quot;, parms={}, **props):
@@ -291,10 +310,10 @@ class HtmlResource(resource.Resource):
     def path_to_root(self, request):
         return path_to_root(request)
 
-    def footer(self, s, req):
+    def footer(self, status, req):
         # TODO: this stuff should be generated by a template of some sort
-        projectURL = s.getProjectURL()
-        projectName = s.getProjectName()
+        projectURL = status.getProjectURL()
+        projectName = status.getProjectName()
         data = '&lt;hr /&gt;&lt;div class=&quot;footer&quot;&gt;\n'
 
         welcomeurl = self.path_to_root(req) + &quot;index.html&quot;
@@ -338,7 +357,7 @@ class HtmlResource(resource.Resource):
         data += &quot;&lt;head&gt;\n&quot;
         for he in s.head_elements:
             data += &quot; &quot; + self.fillTemplate(he, request) + &quot;\n&quot;
-            data += self.head(request)
+        data += self.head(request)
         data += &quot;&lt;/head&gt;\n\n&quot;
 
         data += '&lt;body %s&gt;\n' % &quot; &quot;.join(['%s=&quot;%s&quot;' % (k,v)</diff>
      <filename>buildbot/status/web/base.py</filename>
    </modified>
    <modified>
      <diff>@@ -16,6 +16,7 @@ from buildbot.status.web.base import HtmlResource, Box, \
 from buildbot.status.web.feeds import Rss20StatusResource, \
      Atom10StatusResource
 from buildbot.status.web.waterfall import WaterfallStatusResource
+from buildbot.status.web.console import ConsoleStatusResource
 from buildbot.status.web.grid import GridStatusResource, TransposedGridStatusResource
 from buildbot.status.web.changes import ChangesResource
 from buildbot.status.web.builder import BuildersResource
@@ -123,10 +124,12 @@ class OneLinePerBuild(HtmlResource, OneLineMixin):
         data = &quot;&quot;
 
         # really this is &quot;up to %d builds&quot;
+        html_branches = map(html.escape, branches)
         data += &quot;&lt;h1&gt;Last %d finished builds: %s&lt;/h1&gt;\n&quot; % \
-                (numbuilds, &quot;, &quot;.join(branches))
+                (numbuilds, &quot;, &quot;.join(html_branches))
         if builders:
-            data += (&quot;&lt;p&gt;of builders: %s&lt;/p&gt;\n&quot; % (&quot;, &quot;.join(builders)))
+            html_builders = map(html.escape, builders)
+            data += (&quot;&lt;p&gt;of builders: %s&lt;/p&gt;\n&quot; % (&quot;, &quot;.join(html_builders)))
         data += &quot;&lt;ul&gt;\n&quot;
         got = 0
         building = False
@@ -179,8 +182,9 @@ class OneLinePerBuildOneBuilder(HtmlResource, OneLineMixin):
                                                 numbuilds)
 
         data = &quot;&quot;
+        html_branches = map(html.escape, branches)
         data += (&quot;&lt;h1&gt;Last %d builds of builder %s: %s&lt;/h1&gt;\n&quot; %
-                 (numbuilds, self.builder_name, &quot;, &quot;.join(branches)))
+                 (numbuilds, self.builder_name, &quot;, &quot;.join(html_branches)))
         data += &quot;&lt;ul&gt;\n&quot;
         got = 0
         for build in g:
@@ -215,7 +219,8 @@ class OneBoxPerBuilder(HtmlResource):
 
         data = &quot;&quot;
 
-        data += &quot;&lt;h2&gt;Latest builds: %s&lt;/h2&gt;\n&quot; % &quot;, &quot;.join(branches)
+        html_branches = map(html.escape, branches)
+        data += &quot;&lt;h2&gt;Latest builds: %s&lt;/h2&gt;\n&quot; % &quot;, &quot;.join(html_branches)
         data += &quot;&lt;table&gt;\n&quot;
 
         building = False
@@ -510,6 +515,7 @@ class WebStatus(service.MultiService):
         #self.putChild(&quot;&quot;, IndexOrWaterfallRedirection())
         self.putChild(&quot;waterfall&quot;, WaterfallStatusResource())
         self.putChild(&quot;grid&quot;, GridStatusResource())
+        self.putChild(&quot;console&quot;, ConsoleStatusResource())
         self.putChild(&quot;tgrid&quot;, TransposedGridStatusResource())
         self.putChild(&quot;builders&quot;, BuildersResource()) # has builds/steps/logs
         self.putChild(&quot;changes&quot;, ChangesResource())</diff>
      <filename>buildbot/status/web/baseweb.py</filename>
    </modified>
    <modified>
      <diff>@@ -6,7 +6,9 @@ from twisted.internet import defer, reactor
 import urllib, time
 from twisted.python import log
 from buildbot.status.web.base import HtmlResource, make_row, make_stop_form, \
-     css_classes, path_to_builder, path_to_slave, make_name_user_passwd_form
+     make_extra_property_row, css_classes, path_to_builder, path_to_slave, \
+     make_name_user_passwd_form
+
 
 from buildbot.status.web.tests import TestsResource
 from buildbot.status.web.step import StepsResource
@@ -30,8 +32,8 @@ class StatusResourceBuild(HtmlResource):
     def body(self, req):
         b = self.build_status
         status = self.getStatus(req)
-        projectURL = status.getProjectURL()
         projectName = status.getProjectName()
+        projectURL = status.getProjectURL()
         data = ('&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;&lt;/div&gt;\n'
                 % (self.path_to_root(req), projectName))
         builder_name = b.getBuilder().getName()
@@ -123,8 +125,8 @@ class StatusResourceBuild(HtmlResource):
                         name,
                         &quot; &quot;.join(s.getText()),
                         time_to_run))
+            data += &quot;  &lt;ol&gt;\n&quot;
             if s.getLogs():
-                data += &quot;  &lt;ol&gt;\n&quot;
                 for logfile in s.getLogs():
                     logname = logfile.getName()
                     logurl = req.childLink(&quot;steps/%s/logs/%s&quot; %
@@ -132,8 +134,15 @@ class StatusResourceBuild(HtmlResource):
                                             urllib.quote(logname)))
                     data += (&quot;   &lt;li&gt;&lt;a href=\&quot;%s\&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;\n&quot; %
                              (logurl, logfile.getName()))
-                data += &quot;  &lt;/ol&gt;\n&quot;
+            if s.getURLs():
+                for url in s.getURLs().items():
+                    logname = url[0]
+                    logurl = url[1]
+                    data += ('   &lt;li&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;\n' %
+                             (logurl, html.escape(logname)))
+            data += &quot;&lt;/ol&gt;\n&quot;
             data += &quot; &lt;/li&gt;\n&quot;
+
         data += &quot;&lt;/ol&gt;\n&quot;
 
         data += &quot;&lt;h2&gt;Build Properties:&lt;/h2&gt;\n&quot;
@@ -199,6 +208,9 @@ class StatusResourceBuild(HtmlResource):
             data += ('&lt;form method=&quot;post&quot; action=&quot;%s&quot; class=&quot;command rebuild&quot;&gt;\n'
                      % rebuildURL)
             data += make_name_user_passwd_form(self.isUsingUserPasswd(req))
+            data += make_extra_property_row(1)
+            data += make_extra_property_row(2)
+            data += make_extra_property_row(3)
             data += make_row(&quot;Reason for re-running build:&quot;,
                              &quot;&lt;input type='text' name='comments' /&gt;&quot;)
             data += '&lt;input type=&quot;submit&quot; value=&quot;Rebuild&quot; /&gt;\n'
@@ -239,14 +251,14 @@ class StatusResourceBuild(HtmlResource):
                 (b.getBuilder().getName(), b.getNumber()))
         name = req.args.get(&quot;username&quot;, [&quot;&lt;unknown&gt;&quot;])[0]
         comments = req.args.get(&quot;comments&quot;, [&quot;&lt;no reason specified&gt;&quot;])[0]
+        # html-quote both the username and comments, just to be safe
         reason = (&quot;The web-page 'stop build' button was pressed by &quot;
-                  &quot;'%s': %s\n&quot; % (name, comments))
+                  &quot;'%s': %s\n&quot; % (html.escape(name), html.escape(comments)))
         if c:
             c.stopBuild(reason)
         # we're at http://localhost:8080/svn-hello/builds/5/stop?[args] and
         # we want to go to: http://localhost:8080/svn-hello
-        url = req.args.get('url', ['../..'])[0]
-        r = Redirect(url)
+        r = Redirect(&quot;../..&quot;)
         d = defer.Deferred()
         reactor.callLater(1, d.callback, r)
         return DeferredResource(d)
@@ -262,7 +274,7 @@ class StatusResourceBuild(HtmlResource):
         name = req.args.get(&quot;username&quot;, [&quot;&lt;unknown&gt;&quot;])[0]
         comments = req.args.get(&quot;comments&quot;, [&quot;&lt;no reason specified&gt;&quot;])[0]
         reason = (&quot;The web-page 'rebuild' button was pressed by &quot;
-                  &quot;'%s': %s\n&quot; % (name, comments))
+                  &quot;'%s': %s\n&quot; % (html.escape(name), html.escape(comments)))
         if not bc or not b.isFinished():
             log.msg(&quot;could not rebuild: bc=%s, isFinished=%s&quot;
                     % (bc, b.isFinished()))</diff>
      <filename>buildbot/status/web/build.py</filename>
    </modified>
    <modified>
      <diff>@@ -10,6 +10,7 @@ from buildbot.status.web.base import HtmlResource, make_row, \
      make_force_build_form, OneLineMixin, path_to_build, path_to_slave, \
      path_to_builder, path_to_change
 from buildbot.process.base import BuildRequest
+from buildbot.process.properties import Properties
 from buildbot.sourcestamp import SourceStamp
 
 from buildbot.status.web.build import BuildsResource, StatusResourceBuild
@@ -115,7 +116,8 @@ class StatusResourceBuilder(HtmlResource, OneLineMixin):
             data += &quot;&lt;/ul&gt;\n&quot;
 
             cancelURL = path_to_builder(req, self.builder_status) + '/cancelbuild'
-            data += '''
+            if self.builder_control is not None:
+                data += '''
 &lt;form action=&quot;%s&quot; class=&quot;command cancelbuild&quot; style=&quot;display:inline&quot; method=&quot;post&quot;&gt;
   &lt;input type=&quot;hidden&quot; name=&quot;id&quot; value=&quot;all&quot; /&gt;
   &lt;input type=&quot;submit&quot; value=&quot;Cancel All&quot; /&gt;
@@ -129,7 +131,7 @@ class StatusResourceBuilder(HtmlResource, OneLineMixin):
         data += &quot;&lt;h2&gt;Recent Builds:&lt;/h2&gt;\n&quot;
         data += &quot;(&lt;a href=\&quot;%s\&quot;&gt;view in waterfall&lt;/a&gt;)\n&quot; % (self.path_to_root(req)+&quot;waterfall?show=&quot;+html.escape(b.getName()))
         data += &quot;&lt;ul&gt;\n&quot;
-        numbuilds = req.args.get('numbuilds', ['5'])[0]
+        numbuilds = int(req.args.get('numbuilds', ['5'])[0])
         for i,build in enumerate(b.generateFinishedBuilds(num_builds=int(numbuilds))):
             data += &quot; &lt;li&gt;&quot; + self.make_line(req, build, False) + &quot;&lt;/li&gt;\n&quot;
             if i == 0:
@@ -192,9 +194,15 @@ class StatusResourceBuilder(HtmlResource, OneLineMixin):
         reason = req.args.get(&quot;comments&quot;, [&quot;&lt;no reason specified&gt;&quot;])[0]
         branch = req.args.get(&quot;branch&quot;, [&quot;&quot;])[0]
         revision = req.args.get(&quot;revision&quot;, [&quot;&quot;])[0]
+        properties = Properties()
+        for i in (1,2,3):
+            pname = req.args.get(&quot;prop%dname&quot; % i, [&quot;&quot;])[0]
+            pvalue = req.args.get(&quot;prop%dvalue&quot; % i, [&quot;&quot;])[0]
+            if pname and pvalue:
+                properties.setProperty(pname, pvalue, &quot;Force Build Form&quot;)
 
         r = &quot;The web-page 'force build' button was pressed by '%s': %s\n&quot; \
-            % (name, reason)
+            % (html.escape(name), html.escape(reason))
         log.msg(&quot;web forcebuild of builder '%s', branch='%s', revision='%s'&quot;
                 &quot; by user '%s'&quot; % (self.builder_status.getName(), branch,
                                    revision, name))
@@ -208,14 +216,21 @@ class StatusResourceBuilder(HtmlResource, OneLineMixin):
             if not self.authUser(req):
                 return Redirect(&quot;../../authfail&quot;)
 
-        # keep weird stuff out of the branch and revision strings. TODO:
-        # centralize this somewhere.
+        # keep weird stuff out of the branch revision, and property strings.
+        # TODO: centralize this somewhere.
         if not re.match(r'^[\w\.\-\/]*$', branch):
             log.msg(&quot;bad branch '%s'&quot; % branch)
             return Redirect(&quot;..&quot;)
         if not re.match(r'^[\w\.\-\/]*$', revision):
             log.msg(&quot;bad revision '%s'&quot; % revision)
             return Redirect(&quot;..&quot;)
+        for p in properties.asList():
+            key = p[0]
+            value = p[1]
+            if not re.match(r'^[\w\.\-\/]*$', key) \
+              or not re.match(r'^[\w\.\-\/]*$', value):
+                log.msg(&quot;bad property name='%s', value='%s'&quot; % (key, value))
+                return Redirect(&quot;..&quot;)
         if not branch:
             branch = None
         if not revision:
@@ -229,7 +244,8 @@ class StatusResourceBuilder(HtmlResource, OneLineMixin):
         # buildbot.changes.changes.Change instance which is tedious at this
         # stage to compute
         s = SourceStamp(branch=branch, revision=revision)
-        req = BuildRequest(r, s, builderName=self.builder_status.getName())
+        req = BuildRequest(r, s, builderName=self.builder_status.getName(),
+                           properties=properties)
         try:
             self.builder_control.requestBuildSoon(req)
         except interfaces.NoSlaveError:</diff>
      <filename>buildbot/status/web/builder.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,3 +1,4 @@
+from twisted.web import html, resource
 from buildbot.status.web.base import Box
 from buildbot.status.web.base import HtmlResource
 from buildbot.status.web.base import IBox
@@ -22,6 +23,7 @@ class BuildStatusStatusResource(HtmlResource):
         number = request.args.get(&quot;number&quot;, [None])[0]
         if not name or not number:
             return &quot;builder and number parameter missing&quot;
+        number = int(number)
 
         # Main table for the build status.
         data += '&lt;table&gt;\n'</diff>
      <filename>buildbot/status/web/buildstatus.py</filename>
    </modified>
    <modified>
      <diff>@@ -85,6 +85,221 @@ td.idle, td.waiting, td.offline, td.building {
 	-moz-border-radius-topright: 5px;
 }
 
+/* Console view styles */
+
+td.DevRev {
+        padding: 4px 8px 4px 8px;
+        color: #333333;
+        border-top-left-radius: 5px;
+        -webkit-border-top-left-radius: 5px;
+        -moz-border-radius-topleft: 5px;
+        background-color: #eee;
+        width: 1%;
+}
+
+td.DevRevCollapse {
+        border-bottom-left-radius: 5px;
+        -webkit-border-bottom-left-radius: 5px;
+        -moz-border-radius-bottomleft: 5px;
+}
+
+td.DevName {
+        padding: 4px 8px 4px 8px;
+        color: #333333;
+        background-color: #eee;
+        width: 1%;
+        text-align: left;
+}
+
+td.DevStatus {
+        padding: 4px 4px 4px 4px;
+        color: #333333;
+        background-color: #eee;
+}
+
+td.DevSlave {
+        padding: 4px 4px 4px 4px;
+        color: #333333;
+        background-color: #eee;
+}
+
+td.first {
+        border-top-left-radius: 5px;
+        -webkit-border-top-left-radius: 5px;
+        -moz-border-radius-topleft: 5px;
+}
+
+td.last {
+        border-top-right-radius: 5px;
+        -webkit-border-top-right-radius: 5px;
+        -moz-border-radius-topright: 5px;
+}
+
+td.DevStatusCategory {
+        border-radius: 5px;
+        -webkit-border-radius: 5px;
+        -moz-border-radius: 5px;
+        border-width:1px;
+        border-style:solid;
+}
+
+td.DevStatusCollapse {
+        border-bottom-right-radius: 5px;
+        -webkit-border-bottom-right-radius: 5px;
+        -moz-border-radius-bottomright: 5px;
+}
+
+td.DevDetails {
+        font-weight: normal;
+        padding: 8px 8px 8px 8px;
+        color: #333333;
+        background-color: #eee;
+        text-align: left;
+}
+
+td.DevComment {
+        font-weight: normal;
+        padding: 8px 8px 8px 8px;
+        color: #333333;
+        border-bottom-right-radius: 5px;
+        -webkit-border-bottom-right-radius: 5px;
+        -moz-border-radius-bottomright: 5px;
+        border-bottom-left-radius: 5px;
+        -webkit-border-bottom-left-radius: 5px;
+        -moz-border-radius-bottomleft: 5px;
+        background-color: #eee;
+        text-align: left;
+}
+
+td.Alt {
+        background-color: #CCCCCC;
+}
+
+.legend {
+        border-radius: 5px;
+        -webkit-border-radius: 5px;
+        -moz-border-radius: 5px;
+        width: 100px;
+        max-width: 100px;
+        text-align:center;
+        padding: 2px 2px 2px 2px;
+        height:14px;
+        white-space:nowrap;
+}
+
+.DevStatusBox {
+        text-align:center;
+        height:20px;
+        padding:0 2px;
+        line-height:0;
+        white-space:nowrap;
+}
+
+.DevStatusBox a {
+        opacity: 0.85;
+        border-width:1px;
+        border-style:solid;
+        border-radius: 4px;
+        -webkit-border-radius: 4px;
+        -moz-border-radius: 4px;
+        display:block;
+        width:90%;
+        height:20px;
+        line-height:20px;
+        margin-left: auto;
+        margin-right: auto;
+}
+
+.DevSlaveBox {
+        text-align:center;
+        height:10px;
+        padding:0 2px;
+        line-height:0;
+        white-space:nowrap;
+}
+
+.DevSlaveBox a {
+        opacity: 0.85;
+        border-width:1px;
+        border-style:solid;
+        border-radius: 4px;
+        -webkit-border-radius: 4px;
+        -moz-border-radius: 4px;
+        display:block;
+        width:90%;
+        height:10px;
+        line-height:20px;
+        margin-left: auto;
+        margin-right: auto;
+}
+
+a.noround {
+        border-radius: 0px;
+        -webkit-border-radius: 0px;
+        -moz-border-radius: 0px;
+        position: relative;
+        margin-top: -8px;
+        margin-bottom: -8px;
+        height: 36px;
+        border-top-width: 0;
+        border-bottom-width: 0;
+}
+
+a.begin {
+        border-top-width:1px;
+        position: relative;
+        margin-top: 0px;
+        margin-bottom: -7px;
+        height: 27px;
+	border-top-left-radius: 4px;
+	-webkit-border-top-left-radius: 4px;
+        -moz-border-radius-topleft: 4px;
+	border-top-right-radius: 4px;
+	-webkit-border-top-right-radius: 4px;
+        -moz-border-radius-topright: 4px;
+}
+
+a.end {
+        border-bottom-width:1px;
+        position: relative;
+        margin-top: -7px;
+        margin-bottom: 0px;
+        height: 27px;
+	border-bottom-left-radius: 4px;
+	-webkit-border-bottom-left-radius: 4px;
+        -moz-border-radius-bottomleft: 4px;
+	border-bottom-right-radius: 4px;
+	-webkit-border-bottom-right-radius: 4px;
+        -moz-border-radius-bottomright: 4px;
+}
+
+.center_align {
+        text-align: center;
+}
+
+.right_align {
+        text-align: right;
+}
+
+.left_align {
+        text-align: left;
+}
+
+div.BuildWaterfall {
+	border-radius: 7px;
+	-webkit-border-radius: 7px;
+	-moz-border-radius: 7px;
+        position: absolute;
+        left: 0px;
+        top: 0px;
+        background-color: #FFFFFF;
+        padding: 4px 4px 4px 4px;
+        float: left;
+        display: none;
+        border-width: 1px;
+        border-style: solid;
+}
+
 /* LastBuild, BuildStep states */
 .success {
 	color: #FFFFFF;
@@ -165,7 +380,3 @@ table.Grid tr td.builder {
 table.Grid tr td.build {
         border: 1px gray solid;
 }
-
-div.footer {
-        font-size: 80%;
-}</diff>
      <filename>buildbot/status/web/extended.css</filename>
    </modified>
    <modified>
      <diff>@@ -27,7 +27,7 @@ import os
 import re
 import sys
 import time
-from twisted.web import resource
+from twisted.web import resource, html
 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
 
 class XmlResource(resource.Resource):
@@ -186,22 +186,6 @@ class FeedResource(XmlResource):
             title = ('%s failed on &quot;%s&quot;' %
                      (source, build.getBuilder().getName()))
 
-            # get name of the failed step and the last 30 lines of its log.
-            if build.getLogs():
-                log = build.getLogs()[-1]
-                laststep = log.getStep().getName()
-                try:
-                    lastlog = log.getText()
-                except IOError:
-                    # Probably the log file has been removed
-                    lastlog='&lt;b&gt;log file not available&lt;/b&gt;'
-
-            lines = re.split('\n', lastlog)
-            lastlog = ''
-            for logline in lines[max(0, len(lines)-30):]:
-                lastlog = lastlog + logline + '&lt;br/&gt;'
-            lastlog = lastlog.replace('\n', '&lt;br/&gt;')
-
             description = ''
             description += ('Date: %s&lt;br/&gt;&lt;br/&gt;' %
                             time.strftime(&quot;%a, %d %b %Y %H:%M:%S GMT&quot;,
@@ -218,8 +202,26 @@ class FeedResource(XmlResource):
                             (link, link))
             description += ('Author list: &lt;b&gt;%s&lt;/b&gt;&lt;br/&gt;&lt;br/&gt;' %
                             &quot;,&quot;.join(build.getResponsibleUsers()))
-            description += ('Failed step: &lt;b&gt;%s&lt;/b&gt;&lt;br/&gt;&lt;br/&gt;' % laststep)
-            description += 'Last lines of the build log:&lt;br/&gt;'
+
+            # Add information about the failing steps.
+            lastlog = ''
+            for s in build.getSteps():
+                if s.getResults()[0] == FAILURE:
+                    description += ('Failed step: &lt;b&gt;%s&lt;/b&gt;&lt;br/&gt;' % s.getName())
+
+                    # Add the last 30 lines of each log.
+                    for log in s.getLogs():
+                        lastlog += ('Last lines of build log &quot;%s&quot;:&lt;br/&gt;' % log.getName())
+                        try:
+                            logdata = log.getText()
+                        except IOError:
+                            # Probably the log file has been removed
+                            logdata ='&lt;b&gt;log file not available&lt;/b&gt;'
+
+                        lastlines = logdata.split('\n')[-30:]
+                        lastlog += '&lt;br/&gt;'.join(lastlines)
+                        lastlog += '&lt;br/&gt;'
+            description += '&lt;br/&gt;'
 
             data += self.item(title, description=description, lastlog=lastlog,
                               link=link, pubDate=finishedTime)
@@ -263,12 +265,8 @@ class Rss20StatusResource(FeedResource):
         if link is not None:
             data += ('        &lt;link&gt;%s&lt;/link&gt;\n' % link)
         if (description is not None and lastlog is not None):
-            lastlog = re.sub(r'&lt;br/&gt;', &quot;\n&quot;, lastlog)
-            lastlog = re.sub(r'&amp;', &quot;&amp;amp;&quot;, lastlog)
-            lastlog = re.sub(r&quot;'&quot;, &quot;&amp;apos;&quot;, lastlog)
-            lastlog = re.sub(r'&quot;', &quot;&amp;quot;&quot;, lastlog)
-            lastlog = re.sub(r'&lt;', '&amp;lt;', lastlog)
-            lastlog = re.sub(r'&gt;', '&amp;gt;', lastlog)
+            lastlog = lastlog.replace('&lt;br/&gt;', '\n')
+            lastlog = html.escape(lastlog)
             lastlog = lastlog.replace('\n', '&lt;br/&gt;')
             content = '&lt;![CDATA['
             content += description
@@ -328,12 +326,9 @@ class Atom10StatusResource(FeedResource):
         if link is not None:
             data += ('    &lt;link href=&quot;%s&quot;/&gt;\n' % link)
         if (description is not None and lastlog is not None):
-            lastlog = re.sub(r'&lt;br/&gt;', &quot;\n&quot;, lastlog)
-            lastlog = re.sub(r'&amp;', &quot;&amp;amp;&quot;, lastlog)
-            lastlog = re.sub(r&quot;'&quot;, &quot;&amp;apos;&quot;, lastlog)
-            lastlog = re.sub(r'&quot;', &quot;&amp;quot;&quot;, lastlog)
-            lastlog = re.sub(r'&lt;', '&amp;lt;', lastlog)
-            lastlog = re.sub(r'&gt;', '&amp;gt;', lastlog)
+            lastlog = lastlog.replace('&lt;br/&gt;', '\n')
+            lastlog = html.escape(lastlog)
+            lastlog = lastlog.replace('\n', '&lt;br/&gt;')
             data += ('    &lt;content type=&quot;xhtml&quot;&gt;\n')
             data += ('      &lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;\n')
             data += ('        %s\n' % description)</diff>
      <filename>buildbot/status/web/feeds.py</filename>
    </modified>
    <modified>
      <diff>@@ -3,6 +3,8 @@ from __future__ import generators
 import sys, time, os.path
 import urllib
 
+from twisted.web import html, resource
+
 from buildbot import util
 from buildbot import version
 from buildbot.status.web.base import HtmlResource
@@ -194,12 +196,13 @@ class GridStatusResource(HtmlResource, GridStatusMixin):
         data += '&lt;tr&gt;\n'
         data += '&lt;td class=&quot;title&quot;&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;' % (projectURL, projectName)
         if categories:
+            html_categories = map(html.escape(categories))
             if len(categories) &gt; 1:
-                data += '\n&lt;br /&gt;&lt;b&gt;Categories:&lt;/b&gt;&lt;br/&gt;%s' % ('&lt;br/&gt;'.join(categories))
+                data += '\n&lt;br /&gt;&lt;b&gt;Categories:&lt;/b&gt;&lt;br/&gt;%s' % ('&lt;br/&gt;'.join(html_categories))
             else:
-                data += '\n&lt;br /&gt;&lt;b&gt;Category:&lt;/b&gt; %s' % categories[0]
+                data += '\n&lt;br /&gt;&lt;b&gt;Category:&lt;/b&gt; %s' % html_categories[0]
         if branch != ANYBRANCH:
-            data += '\n&lt;br /&gt;&lt;b&gt;Branch:&lt;/b&gt; %s' % (branch or 'trunk')
+            data += '\n&lt;br /&gt;&lt;b&gt;Branch:&lt;/b&gt; %s' % (html.escape(branch or 'trunk'))
         data += '&lt;/td&gt;\n'
         for stamp in stamps:
             data += self.stamp_td(stamp)
@@ -289,12 +292,13 @@ class TransposedGridStatusResource(HtmlResource, GridStatusMixin):
         data += '&lt;tr&gt;\n'
         data += '&lt;td class=&quot;title&quot;&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;' % (projectURL, projectName)
         if categories:
+            html_categories = map(html.escape(categories))
             if len(categories) &gt; 1:
-                data += '\n&lt;br /&gt;&lt;b&gt;Categories:&lt;/b&gt;&lt;br/&gt;%s' % ('&lt;br/&gt;'.join(categories))
+                data += '\n&lt;br /&gt;&lt;b&gt;Categories:&lt;/b&gt;&lt;br/&gt;%s' % ('&lt;br/&gt;'.join(html_categories))
             else:
-                data += '\n&lt;br /&gt;&lt;b&gt;Category:&lt;/b&gt; %s' % categories[0]
+                data += '\n&lt;br /&gt;&lt;b&gt;Category:&lt;/b&gt; %s' % html_categories[0]
         if branch != ANYBRANCH:
-            data += '\n&lt;br /&gt;&lt;b&gt;Branch:&lt;/b&gt; %s' % (branch or 'trunk')
+            data += '\n&lt;br /&gt;&lt;b&gt;Branch:&lt;/b&gt; %s' % (html.escape(branch or 'trunk'))
         data += '&lt;/td&gt;\n'
 
         sortedBuilderNames = status.getBuilderNames()[:]</diff>
      <filename>buildbot/status/web/grid.py</filename>
    </modified>
    <modified>
      <diff>@@ -9,15 +9,15 @@
 &lt;h1&gt;Welcome to the Buildbot!&lt;/h1&gt;
 
 &lt;ul&gt;
-  &lt;li&gt;the &lt;a href=&quot;waterfall&quot;&gt;Waterfall Display&lt;/a&gt; will give you a
-  time-oriented summary of recent buildbot activity.&lt;/li&gt;
-
   &lt;li&gt;the &lt;a href=&quot;grid&quot;&gt;Grid Display&lt;/a&gt; will give you a
   developer-oriented summary of recent buildbot activity.&lt;/li&gt;
 
   &lt;li&gt;the &lt;a href=&quot;tgrid&quot;&gt;Transposed Grid Display&lt;/a&gt; presents
   the same information as the grid, but lists the revisions down the side.&lt;/li&gt;
 
+  &lt;li&gt;the &lt;a href=&quot;waterfall&quot;&gt;Waterfall Display&lt;/a&gt; will give you a
+  time-oriented summary of recent buildbot activity.&lt;/li&gt;
+
   &lt;li&gt;The &lt;a href=&quot;one_box_per_builder&quot;&gt;Latest Build&lt;/a&gt; for each builder is
   here.&lt;/li&gt;
 </diff>
      <filename>buildbot/status/web/index.html</filename>
    </modified>
    <modified>
      <diff>@@ -77,7 +77,7 @@ class OneBuildSlaveResource(HtmlResource, OneLineMixin):
 
         data.append(&quot;&lt;a href=\&quot;%s\&quot;&gt;%s&lt;/a&gt;\n&quot; % (self.path_to_root(req), projectName))
 
-        data.append(&quot;&lt;h1&gt;Build Slave: %s&lt;/h1&gt;\n&quot; % self.slavename)
+        data.append(&quot;&lt;h1&gt;Build Slave: %s&lt;/h1&gt;\n&quot; % html.escape(self.slavename))
 
         shutdown_url = req.childLink(&quot;shutdown&quot;)
 
@@ -170,6 +170,8 @@ class BuildSlavesResource(HtmlResource):
             isBusy = len(slave_status.getRunningBuilds())
             data += &quot; &lt;li&gt;&lt;a href=\&quot;%s\&quot;&gt;%s&lt;/a&gt;:\n&quot; % (req.childLink(urllib.quote(name,'')), name)
             data += &quot; &lt;ul&gt;\n&quot;
+            version = slave.getVersion()
+            data += &quot;&lt;li&gt;Running Buildbot version: %s&quot; % version
             builder_links = ['&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;'
                              % (req.childLink(&quot;../builders/%s&quot; % bname),bname)
                              for bname in used_by_builder.get(name, [])]
@@ -213,4 +215,4 @@ class BuildSlavesResource(HtmlResource):
             slave = self.getStatus(req).getSlave(path)
             return OneBuildSlaveResource(path)
         except KeyError:
-            return NoResource(&quot;No such slave '%s'&quot; % path)
+            return NoResource(&quot;No such slave '%s'&quot; % html.escape(path))</diff>
      <filename>buildbot/status/web/slaves.py</filename>
    </modified>
    <modified>
      <diff>@@ -61,4 +61,4 @@ class TestsResource(HtmlResource):
             result = self.test_results[name]
             return TestResult(name, result)
         except KeyError:
-            return NoResource(&quot;No such test name '%s'&quot; % path)
+            return NoResource(&quot;No such test name '%s'&quot; % html.escape(path))</diff>
      <filename>buildbot/status/web/tests.py</filename>
    </modified>
    <modified>
      <diff>@@ -263,7 +263,7 @@ be displayed that occurred &lt;b&gt;before&lt;/b&gt; this timestamp. Instead of providing
 given interval, where each timestamp on the left hand edge counts as a single
 event. You can add a &lt;tt&gt;num_events=&lt;/tt&gt; argument to override this this.&lt;/p&gt;
 
-&lt;h2&gt;Hiding non-Build events&lt;/h2&gt;
+&lt;h2&gt;Showing non-Build events&lt;/h2&gt;
 
 &lt;p&gt;By passing &lt;tt&gt;show_events=true&lt;/tt&gt;, you can add the &quot;buildslave
 attached&quot;, &quot;buildslave detached&quot;, and &quot;builder reconfigured&quot; events that
@@ -327,13 +327,13 @@ class WaterfallHelp(HtmlResource):
         data = ''
         status = self.getStatus(request)
 
-        showEvents_checked = 'checked=&quot;checked&quot;'
+        showEvents_checked = ''
         if request.args.get(&quot;show_events&quot;, [&quot;false&quot;])[0].lower() == &quot;true&quot;:
-            showEvents_checked = ''
+            showEvents_checked = 'checked=&quot;checked&quot;'
         show_events_input = ('&lt;p&gt;'
                              '&lt;input type=&quot;checkbox&quot; name=&quot;show_events&quot; '
                              'value=&quot;true&quot; %s&gt;'
-                             'Hide non-Build events'
+                             'Show non-Build events'
                              '&lt;/p&gt;\n'
                              ) % showEvents_checked
 
@@ -358,7 +358,7 @@ class WaterfallHelp(HtmlResource):
                                     '&lt;input type=&quot;text&quot; name=&quot;branch&quot; '
                                     'value=&quot;%s&quot;&gt;'
                                     '&lt;/td&gt;&lt;/tr&gt;\n'
-                                    ) % (b,)
+                                    ) % (html.escape(b),)
         show_branches_input += '&lt;/table&gt;\n'
 
         # this has a set of toggle-buttons to let the user choose the
@@ -401,7 +401,7 @@ class WaterfallHelp(HtmlResource):
                                   '&lt;td&gt;&lt;input type=&quot;radio&quot; name=&quot;reload&quot; '
                                   'value=&quot;%s&quot; %s&gt;&lt;/td&gt; '
                                   '&lt;td&gt;%s&lt;/td&gt;&lt;/tr&gt;\n'
-                                  ) % (value, checked, name)
+                                  ) % (html.escape(value), checked, html.escape(name))
         show_reload_input += '&lt;/table&gt;\n'
 
         fields = {&quot;show_events_input&quot;: show_events_input,
@@ -586,7 +586,7 @@ class WaterfallStatusResource(HtmlResource):
                     newargs[k].append(v)
                 else:
                     newargs[k] = [v]
-            newquery = &quot;&amp;&quot;.join([&quot;%s=%s&quot; % (k, v)
+            newquery = &quot;&amp;&quot;.join([&quot;%s=%s&quot; % (urllib.quote(k), urllib.quote(v))
                                  for k in newargs
                                  for v in newargs[k]
                                  ])</diff>
      <filename>buildbot/status/web/waterfall.py</filename>
    </modified>
    <modified>
      <diff>@@ -53,7 +53,6 @@ class IrcBuildRequest:
         d = s.waitUntilFinished()
         d.addCallback(self.parent.watchedBuildFinished)
 
-
 class Contact:
     &quot;&quot;&quot;I hold the state for a single user's interaction with the buildbot.
 
@@ -307,16 +306,16 @@ class Contact:
 
         self.send(r)
 
+    results_descriptions = {
+        SUCCESS: &quot;Success&quot;,
+        WARNINGS: &quot;Warnings&quot;,
+        FAILURE: &quot;Failure&quot;,
+        EXCEPTION: &quot;Exception&quot;,
+        }
+
     def buildFinished(self, builderName, build, results):
         builder = build.getBuilder()
 
-        results_descriptions = {
-            SUCCESS: &quot;Success&quot;,
-            WARNINGS: &quot;Warnings&quot;,
-            FAILURE: &quot;Failure&quot;,
-            EXCEPTION: &quot;Exception&quot;,
-            }
-
         # only notify about builders we are interested in
         log.msg('[Contact] builder %r in category %s finished' % (builder, builder.category))
 
@@ -324,39 +323,47 @@ class Contact:
             builder.category not in self.channel.categories):
             return
 
-        results = build.getResults()
+        if not self.notify_for_finished(build):
+            return
 
         r = &quot;build #%d of %s is complete: %s&quot; % \
             (build.getNumber(),
              builder.getName(),
-             results_descriptions.get(results, &quot;??&quot;))
+             self.results_descriptions.get(build.getResults(), &quot;??&quot;))
         r += &quot; [%s]&quot; % &quot; &quot;.join(build.getText())
         buildurl = self.channel.status.getURLForThing(build)
         if buildurl:
             r += &quot;  Build details are at %s&quot; % buildurl
 
-        if self.notify_for('finished') or self.notify_for(lower(results_descriptions.get(results))):
-            self.send(r)
-            return
+        if self.channel.showBlameList and build.getResults() != SUCCESS and len(build.changes) != 0:
+            r += '  blamelist: ' + ', '.join([c.who for c in build.changes])
+
+        self.send(r)
+
+    def notify_for_finished(self, build):
+        results = build.getResults()
+
+        if self.notify_for('finished'):
+            return True
+
+        if self.notify_for(lower(self.results_descriptions.get(results))):
+            return True
 
         prevBuild = build.getPreviousBuild()
         if prevBuild:
             prevResult = prevBuild.getResults()
 
-            required_notification_control_string = join((lower(results_descriptions.get(prevResult)), \
+            required_notification_control_string = join((lower(self.results_descriptions.get(prevResult)), \
                                                              'To', \
-                                                             capitalize(results_descriptions.get(results))), \
+                                                             capitalize(self.results_descriptions.get(results))), \
                                                             '')
 
             if (self.notify_for(required_notification_control_string)):
-                self.send(r)
+                return True
+
+        return False
 
     def watchedBuildFinished(self, b):
-        results = {SUCCESS: &quot;Success&quot;,
-                   WARNINGS: &quot;Warnings&quot;,
-                   FAILURE: &quot;Failure&quot;,
-                   EXCEPTION: &quot;Exception&quot;,
-                   }
 
         # only notify about builders we are interested in
         builder = b.getBuilder()
@@ -369,7 +376,7 @@ class Contact:
         r = &quot;Hey! build %s #%d is complete: %s&quot; % \
             (b.getBuilder().getName(),
              b.getNumber(),
-             results.get(b.getResults(), &quot;??&quot;))
+             self.results_descriptions.get(b.getResults(), &quot;??&quot;))
         r += &quot; [%s]&quot; % &quot; &quot;.join(b.getText())
         self.send(r)
         buildurl = self.channel.status.getURLForThing(b)
@@ -581,14 +588,15 @@ class IRCContact(Contact):
         self.dest = dest
 
     def describeUser(self, user):
-        if self.dest[0] == &quot;#&quot;:
+        if self.dest[0] == '#':
             return &quot;IRC user &lt;%s&gt; on channel %s&quot; % (user, self.dest)
         return &quot;IRC user &lt;%s&gt; (privmsg)&quot; % user
 
     # userJoined(self, user, channel)
 
     def send(self, message):
-        self.channel.msg(self.dest, message.encode(&quot;ascii&quot;, &quot;replace&quot;))
+        self.channel.msgOrNotice(self.dest, message.encode(&quot;ascii&quot;, &quot;replace&quot;))
+
     def act(self, action):
         self.channel.me(self.dest, action.encode(&quot;ascii&quot;, &quot;replace&quot;))
 
@@ -661,8 +669,9 @@ class IrcStatusBot(irc.IRCClient):
     &quot;&quot;&quot;I represent the buildbot to an IRC server.
     &quot;&quot;&quot;
     implements(IChannel)
+    contactClass = IRCContact
 
-    def __init__(self, nickname, password, channels, status, categories, notify_events):
+    def __init__(self, nickname, password, channels, status, categories, notify_events, noticeOnChannel = False, showBlameList = False):
         &quot;&quot;&quot;
         @type  nickname: string
         @param nickname: the nickname by which this bot should be known
@@ -683,6 +692,14 @@ class IrcStatusBot(irc.IRCClient):
         self.counter = 0
         self.hasQuit = 0
         self.contacts = {}
+        self.noticeOnChannel = noticeOnChannel
+        self.showBlameList = showBlameList
+
+    def msgOrNotice(self, dest, message):
+        if self.noticeOnChannel and dest[0] == '#':
+            self.notice(dest, message)
+        else:
+            self.msg(dest, message)
 
     def addContact(self, name, contact):
         self.contacts[name] = contact
@@ -690,7 +707,7 @@ class IrcStatusBot(irc.IRCClient):
     def getContact(self, name):
         if name in self.contacts:
             return self.contacts[name]
-        new_contact = IRCContact(self, name)
+        new_contact = self.contactClass(self, name)
         self.contacts[name] = new_contact
         return new_contact
 
@@ -742,6 +759,9 @@ class IrcStatusBot(irc.IRCClient):
 
     def joined(self, channel):
         self.log(&quot;I have joined %s&quot; % (channel,))
+        # trigger contact contructor, which in turn subscribes to notify events
+        self.getContact(channel)
+
     def left(self, channel):
         self.log(&quot;I have left %s&quot; % (channel,))
     def kickedFrom(self, channel, kicker, message):
@@ -774,7 +794,7 @@ class IrcStatusFactory(ThrottledClientFactory):
     shuttingDown = False
     p = None
 
-    def __init__(self, nickname, password, channels, categories, notify_events):
+    def __init__(self, nickname, password, channels, categories, notify_events, noticeOnChannel = False, showBlameList = False):
         #ThrottledClientFactory.__init__(self) # doesn't exist
         self.status = None
         self.nickname = nickname
@@ -782,6 +802,8 @@ class IrcStatusFactory(ThrottledClientFactory):
         self.channels = channels
         self.categories = categories
         self.notify_events = notify_events
+        self.noticeOnChannel = noticeOnChannel
+        self.showBlameList = showBlameList
 
     def __getstate__(self):
         d = self.__dict__.copy()
@@ -796,7 +818,9 @@ class IrcStatusFactory(ThrottledClientFactory):
     def buildProtocol(self, address):
         p = self.protocol(self.nickname, self.password,
                           self.channels, self.status,
-                          self.categories, self.notify_events)
+                          self.categories, self.notify_events,
+                          noticeOnChannel = self.noticeOnChannel,
+                          showBlameList = self.showBlameList)
         p.factory = self
         p.status = self.status
         p.control = self.control
@@ -829,7 +853,8 @@ class IRC(base.StatusReceiverMultiService):
                      &quot;categories&quot;]
 
     def __init__(self, host, nick, channels, port=6667, allowForce=True,
-                 categories=None, password=None, notify_events={}):
+                 categories=None, password=None, notify_events={},
+                 noticeOnChannel = False, showBlameList = True):
         base.StatusReceiverMultiService.__init__(self)
 
         assert allowForce in (True, False) # TODO: implement others
@@ -843,12 +868,12 @@ class IRC(base.StatusReceiverMultiService):
         self.allowForce = allowForce
         self.categories = categories
         self.notify_events = notify_events
-
-        # need to stash the factory so we can give it the status object
+        log.msg('Notify events %s' % notify_events)
         self.f = IrcStatusFactory(self.nick, self.password,
-                                  self.channels, self.categories, self.notify_events)
-
-        c = internet.TCPClient(host, port, self.f)
+                                  self.channels, self.categories, self.notify_events,
+                                  noticeOnChannel = noticeOnChannel,
+                                  showBlameList = showBlameList)
+        c = internet.TCPClient(self.host, self.port, self.f)
         c.setServiceParent(self)
 
     def setServiceParent(self, parent):</diff>
      <filename>buildbot/status/words.py</filename>
    </modified>
    <modified>
      <diff>@@ -35,30 +35,33 @@ class MasterShellCommand(BuildStep):
             self.step.processEnded(status_object)
 
     def start(self):
+        # render properties
+        properties = self.build.getProperties()
+        command = properties.render(self.command)
         # set up argv
-        if type(self.command) in types.StringTypes:
+        if type(command) in types.StringTypes:
             if runtime.platformType  == 'win32':
                 argv = os.environ['COMSPEC'].split() # allow %COMSPEC% to have args
                 if '/c' not in argv: argv += ['/c'] 
-                argv += [self.command]
+                argv += [command]
             else:
                 # for posix, use /bin/sh. for other non-posix, well, doesn't
                 # hurt to try
-                argv = ['/bin/sh', '-c', self.command]
+                argv = ['/bin/sh', '-c', command]
         else:
             if runtime.platformType  == 'win32':
                 argv = os.environ['COMSPEC'].split() # allow %COMSPEC% to have args
                 if '/c' not in argv: argv += ['/c'] 
-                argv += list(self.command)
+                argv += list(command)
             else:
-                argv = self.command
+                argv = command
 
         self.stdio_log = stdio_log = self.addLog(&quot;stdio&quot;)
 
-        if type(self.command) in types.StringTypes:
-            stdio_log.addHeader(self.command.strip() + &quot;\n\n&quot;)
+        if type(command) in types.StringTypes:
+            stdio_log.addHeader(command.strip() + &quot;\n\n&quot;)
         else:
-            stdio_log.addHeader(&quot; &quot;.join(self.command) + &quot;\n\n&quot;)
+            stdio_log.addHeader(&quot; &quot;.join(command) + &quot;\n\n&quot;)
         stdio_log.addHeader(&quot;** RUNNING ON BUILDMASTER **\n&quot;)
         stdio_log.addHeader(&quot; in dir %s\n&quot; % os.getcwd())
         stdio_log.addHeader(&quot; argv: %s\n&quot; % (argv,))</diff>
      <filename>buildbot/steps/master.py</filename>
    </modified>
    <modified>
      <diff>@@ -2,7 +2,9 @@
 
 import re
 from twisted.python import log
+from twisted.spread import pb
 from buildbot.process.buildstep import LoggingBuildStep, RemoteShellCommand
+from buildbot.process.buildstep import RemoteCommand
 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, STDOUT, STDERR
 
 # for existing configurations that import WithProperties from here.  We like
@@ -39,6 +41,12 @@ class ShellCommand(LoggingBuildStep):
                     something approximating real-time. (note that logfiles=
                     is actually handled by our parent class LoggingBuildStep)
 
+    @ivar lazylogfiles: Defaults to False. If True, logfiles will be tracked
+                        `lazily', meaning they will only be added when and if
+                        they are written to. Empty or nonexistent logfiles
+                        will be omitted. (Also handled by class
+                        LoggingBuildStep.)
+
     &quot;&quot;&quot;
 
     name = &quot;shell&quot;
@@ -296,21 +304,191 @@ class Configure(ShellCommand):
     descriptionDone = [&quot;configure&quot;]
     command = [&quot;./configure&quot;]
 
+class StringFileWriter(pb.Referenceable):
+    &quot;&quot;&quot;
+    FileWriter class that just puts received data into a buffer.
+
+    Used to upload a file from slave for inline processing rather than
+    writing into a file on master.
+    &quot;&quot;&quot;
+    def __init__(self):
+        self.buffer = &quot;&quot;
+
+    def remote_write(self, data):
+        self.buffer += data
+
+    def remote_close(self):
+        pass
+
+class SilentRemoteCommand(RemoteCommand):
+    &quot;&quot;&quot;
+    Remote command subclass used to run an internal file upload command on the
+    slave. We do not need any progress updates from such command, so override
+    remoteUpdate() with an empty method.
+    &quot;&quot;&quot;
+    def remoteUpdate(self, update):
+        pass
+
 class WarningCountingShellCommand(ShellCommand):
     warnCount = 0
     warningPattern = '.*warning[: ].*'
+    # The defaults work for GNU Make.
+    directoryEnterPattern = &quot;make.*: Entering directory [\&quot;`'](.*)['`\&quot;]&quot;
+    directoryLeavePattern = &quot;make.*: Leaving directory&quot;
+    suppressionFile = None
 
-    def __init__(self, warningPattern=None, **kwargs):
+    commentEmptyLineRe = re.compile(r&quot;^\s*(\#.*)?$&quot;)
+    suppressionLineRe = re.compile(r&quot;^\s*(.+?)\s*:\s*(.+?)\s*(?:[:]\s*([0-9]+)(?:-([0-9]+))?\s*)?$&quot;)
+
+    def __init__(self, workdir=None,
+                 warningPattern=None, warningExtractor=None,
+                 directoryEnterPattern=None, directoryLeavePattern=None,
+                 suppressionFile=None, **kwargs):
+        self.workdir = workdir
         # See if we've been given a regular expression to use to match
         # warnings. If not, use a default that assumes any line with &quot;warning&quot;
         # present is a warning. This may lead to false positives in some cases.
         if warningPattern:
             self.warningPattern = warningPattern
+        if directoryEnterPattern:
+            self.directoryEnterPattern = directoryEnterPattern
+        if directoryLeavePattern:
+            self.directoryLeavePattern = directoryLeavePattern
+        if suppressionFile:
+            self.suppressionFile = suppressionFile
+        if warningExtractor:
+            self.warningExtractor = warningExtractor
+        else:
+            self.warningExtractor = WarningCountingShellCommand.warnExtractWholeLine
 
         # And upcall to let the base class do its work
-        ShellCommand.__init__(self, **kwargs)
+        ShellCommand.__init__(self, workdir=workdir, **kwargs)
+
+        self.addFactoryArguments(warningPattern=warningPattern,
+                                 directoryEnterPattern=directoryEnterPattern,
+                                 directoryLeavePattern=directoryLeavePattern,
+                                 warningExtractor=warningExtractor,
+                                 suppressionFile=suppressionFile)
+        self.suppressions = []
+        self.directoryStack = []
+
+    def setDefaultWorkdir(self, workdir):
+        if self.workdir is None:
+            self.workdir = workdir
+        ShellCommand.setDefaultWorkdir(self, workdir)
+
+    def addSuppression(self, suppressionList):
+        &quot;&quot;&quot;
+        This method can be used to add patters of warnings that should
+        not be counted.
 
-        self.addFactoryArguments(warningPattern=warningPattern)
+        It takes a single argument, a list of patterns.
+
+        Each pattern is a 4-tuple (FILE-RE, WARN-RE, START, END).
+
+        FILE-RE is a regular expression (string or compiled regexp), or None.
+        If None, the pattern matches all files, else only files matching the
+        regexp. If directoryEnterPattern is specified in the class constructor,
+        matching is against the full path name, eg. src/main.c.
+
+        WARN-RE is similarly a regular expression matched against the
+        text of the warning, or None to match all warnings.
+
+        START and END form an inclusive line number range to match against. If
+        START is None, there is no lower bound, similarly if END is none there
+        is no upper bound.&quot;&quot;&quot;
+
+        for fileRe, warnRe, start, end in suppressionList:
+            if fileRe != None and isinstance(fileRe, str):
+                fileRe = re.compile(fileRe)
+            if warnRe != None and isinstance(warnRe, str):
+                warnRe = re.compile(warnRe)
+            self.suppressions.append((fileRe, warnRe, start, end))
+
+    def warnExtractWholeLine(self, line, match):
+        &quot;&quot;&quot;
+        Extract warning text as the whole line.
+        No file names or line numbers.&quot;&quot;&quot;
+        return (None, None, line)
+
+    def warnExtractFromRegexpGroups(self, line, match):
+        &quot;&quot;&quot;
+        Extract file name, line number, and warning text as groups (1,2,3)
+        of warningPattern match.&quot;&quot;&quot;
+        file = match.group(1)
+        lineNo = match.group(2)
+        if lineNo != None:
+            lineNo = int(lineNo)
+        text = match.group(3)
+        return (file, lineNo, text)
+
+    def maybeAddWarning(self, warnings, line, match):
+        if self.suppressions:
+            (file, lineNo, text) = self.warningExtractor(self, line, match)
+
+            if file != None and file != &quot;&quot; and self.directoryStack:
+                currentDirectory = self.directoryStack[-1]
+                if currentDirectory != None and currentDirectory != &quot;&quot;:
+                    file = &quot;%s/%s&quot; % (currentDirectory, file)
+
+            # Skip adding the warning if any suppression matches.
+            for fileRe, warnRe, start, end in self.suppressions:
+                if ( (file == None or fileRe == None or fileRe.search(file)) and
+                     (warnRe == None or  warnRe.search(text)) and
+                     lineNo != None and
+                     (start == None or start &lt;= lineNo) and
+                     (end == None or end &gt;= lineNo) ):
+                    return
+
+        warnings.append(line)
+        self.warnCount += 1
+
+    def start(self):
+        if self.suppressionFile == None:
+            return ShellCommand.start(self)
+
+        version = self.slaveVersion(&quot;uploadFile&quot;)
+        if not version:
+            m = &quot;Slave is too old, does not know about uploadFile&quot;
+            raise BuildSlaveTooOldError(m)
+
+        self.myFileWriter = StringFileWriter()
+
+        properties = self.build.getProperties()
+
+        args = {
+            'slavesrc': properties.render(self.suppressionFile),
+            'workdir': self.workdir,
+            'writer': self.myFileWriter,
+            'maxsize': None,
+            'blocksize': 32*1024,
+            }
+        cmd = SilentRemoteCommand('uploadFile', args)
+        d = self.runCommand(cmd)
+        d.addCallback(self.uploadDone)
+        d.addErrback(self.failed)
+
+    def uploadDone(self, dummy):
+        lines = self.myFileWriter.buffer.split(&quot;\n&quot;)
+        del(self.myFileWriter)
+
+        list = []
+        for line in lines:
+            if self.commentEmptyLineRe.match(line):
+                continue
+            match = self.suppressionLineRe.match(line)
+            if (match):
+                file, test, start, end = match.groups()
+                if (end != None):
+                    end = int(end)
+                if (start != None):
+                    start = int(start)
+                    if end == None:
+                        end = start
+                list.append((file, test, start, end))
+
+        self.addSuppression(list)
+        return ShellCommand.start(self)
 
     def createSummary(self, log):
         self.warnCount = 0
@@ -324,6 +502,14 @@ class WarningCountingShellCommand(ShellCommand):
         if isinstance(wre, str):
             wre = re.compile(wre)
 
+        directoryEnterRe = self.directoryEnterPattern
+        if directoryEnterRe != None and isinstance(directoryEnterRe, str):
+            directoryEnterRe = re.compile(directoryEnterRe)
+
+        directoryLeaveRe = self.directoryLeavePattern
+        if directoryLeaveRe != None and isinstance(directoryLeaveRe, str):
+            directoryLeaveRe = re.compile(directoryLeaveRe)
+
         # Check if each line in the output from this command matched our
         # warnings regular expressions. If did, bump the warnings count and
         # add the line to the collection of lines with warnings
@@ -331,9 +517,18 @@ class WarningCountingShellCommand(ShellCommand):
         # TODO: use log.readlines(), except we need to decide about stdout vs
         # stderr
         for line in log.getText().split(&quot;\n&quot;):
-            if wre.match(line):
-                warnings.append(line)
-                self.warnCount += 1
+            if directoryEnterRe:
+                match = directoryEnterRe.search(line)
+                if match:
+                    self.directoryStack.append(match.group(1))
+                if (directoryLeaveRe and
+                    self.directoryStack and
+                    directoryLeaveRe.search(line)):
+                        self.directoryStack.pop()
+
+            match = wre.match(line)
+            if match:
+                self.maybeAddWarning(warnings, line, match)
 
         # If there were any warnings, make the log if lines with warnings
         # available</diff>
      <filename>buildbot/steps/shell.py</filename>
    </modified>
    <modified>
      <diff>@@ -42,7 +42,10 @@ class Source(LoggingBuildStep):
              files are deleted but generated files can influence test
              behavior (e.g. python's .pyc files), or when source
              directories are deleted but generated files prevent CVS from
-             removing them.
+             removing them. When used with a patched checkout, from a
+             previous buildbot try for instance, it will try to &quot;revert&quot;
+             the changes first and will do a clobber if it is unable to
+             get a clean checkout. The behavior is SCM-dependent.
 
            - 'copy': specifies that the source-controlled workspace
              should be maintained in a separate directory (called the
@@ -176,11 +179,14 @@ class Source(LoggingBuildStep):
             revision = self.computeSourceRevision(s.changes)
         # if patch is None, then do not patch the tree after checkout
 
-        # 'patch' is None or a tuple of (patchlevel, diff)
+        # 'patch' is None or a tuple of (patchlevel, diff, root)
+        # root is optional.
         patch = s.patch
         if patch:
             self.addCompleteLog(&quot;patch&quot;, patch[1])
 
+        if self.alwaysUseLatest:
+            revision = None
         self.startVC(branch, revision, patch)
 
     def commandComplete(self, cmd):
@@ -1155,4 +1161,3 @@ class Monotone(Source):
         assert slavever, &quot;slave is too old, does not know about monotone&quot;
         cmd = LoggedRemoteCommand(&quot;monotone&quot;, self.args)
         self.startCommand(cmd)
-</diff>
      <filename>buildbot/steps/source.py</filename>
    </modified>
    <modified>
      <diff>@@ -6,6 +6,8 @@ from twisted.trial import unittest
 
 from buildbot import interfaces
 from buildbot.process import buildstep
+from buildbot.process import mtrlogobserver
+from buildbot.process import subunitlogobserver
 
 # have to subclass LogObserver in order to test it, since the default
 # implementations of outReceived() and errReceived() do nothing
@@ -133,6 +135,133 @@ class LogLineObserver(ObserverTestCase):
         self._assertStdout([chunk*4 + &quot;12345&quot;])
         self._assertStderr([])
 
+class MyMtrLogObserver(mtrlogobserver.MtrLogObserver):
+    def __init__(self):
+        mtrlogobserver.MtrLogObserver.__init__(self, textLimit=3, testNameLimit=15)
+        self.testFails = []
+        self.testWarnLists = []
+        # We don't have a buildstep in self.step.
+        # So we'll just install ourself there, so we can check the call of
+        # setProgress().
+        # Same for self.step.step_status.setText()
+        self.step = self
+        self.step_status = self
+        self.progresses = []
+        self.text = []
+
+    def setProgress(self, type, value):
+        self.progresses.append((type, value))
+
+    def setText(self, text):
+        self.text = text
+
+    def collectTestFail(self, testname, variant, result, info, text):
+        self.testFails.append((testname, variant, result, info, text))
+
+    def collectWarningTests(self, testList):
+        self.testWarnLists.append(testList)
+
+class MtrLogObserver(ObserverTestCase):
+    observer_cls = MyMtrLogObserver
+
+    def test1(self):
+        self._logStdout(&quot;&quot;&quot;
+MySQL Version 5.1.35
+==============================================================================
+TEST                                      RESULT   TIME (ms)
+------------------------------------------------------------
+worker[3] Using MTR_BUILD_THREAD 252, with reserved ports 12520..12529
+binlog.binlog_multi_engine               [ skipped ]  No ndbcluster tests(--skip-ndbcluster)
+rpl.rpl_ssl 'row'                        [ pass ]  13976
+***Warnings generated in error logs during shutdown after running tests: rpl.rpl_ssl
+rpl.rpl_ssl 'mix'                        [ pass ]  13308
+main.pool_of_threads                     w1 [ skipped ]  Test requires: 'have_pool_of_threads'
+------------------------------------------------------------
+The servers were restarted 613 times
+mysql-test-run: *** ERROR: There were errors/warnings in server logs after running test cases.
+All 1002 tests were successful.
+
+Errors/warnings were found in logfiles during server shutdown after running the
+following sequence(s) of tests:
+    rpl.rpl_ssl
+&quot;&quot;&quot;)
+        self.assertEqual(self.observer.progresses,
+                         map((lambda (x): ('tests', x)), [1,2]))
+        self.assertEqual(self.observer.testWarnLists, [[&quot;rpl.rpl_ssl&quot;]])
+        self.assertEqual(self.observer.testFails, [])
+        self.assertEqual(self.observer.text[1:], [&quot;W:rpl_ssl&quot;])
+
+    def test2(self):
+        self._logStdout(&quot;&quot;&quot;
+Logging: mysql-test-run.pl  --force --skip-ndb
+==============================================================================
+TEST                                      RESULT   TIME (ms)
+------------------------------------------------------------
+binlog.binlog_multi_engine               [ skipped ]  No ndbcluster tests(--skip-ndbcluster)
+rpl.rpl_sp 'mix'                         [ pass ]   8117
+
+MTR's internal check of the test case 'rpl.rpl_sp' failed.
+This is the diff of the states of the servers before and after the
+test case was executed:
+mysqltest: Logging to '/home/archivist/archivist-cnc/archivist-cnc/build/mysql-test/var/tmp/check-mysqld_2.log'.
+--- /home/archivist/archivist-cnc/archivist-cnc/build/mysql-test/var/tmp/check-mysqld_2.result	2009-06-18 16:49:19.000000000 +0300
++++ /home/archivist/archivist-cnc/archivist-cnc/build/mysql-test/var/tmp/check-mysqld_2.reject	2009-06-18 16:49:29.000000000 +0300
+@@ -523,7 +523,7 @@
+ mysql.help_keyword	864336512
+ mysql.help_relation	2554468794
+ mysql.host	0
+-mysql.proc	3342691386
++mysql.proc	3520745907
+
+not ok
+
+rpl.rpl_sp_effects 'row'                 [ pass ]   3789
+rpl.rpl_temporary_errors 'mix'        w2 [ fail ]
+        Test ended at 2009-06-18 16:21:28
+
+CURRENT_TEST: rpl.rpl_temporary_errors
+Retrying test, attempt(2/3)...
+
+***Warnings generated in error logs during shutdown after running tests: rpl.rpl_temporary_errors
+rpl.rpl_temporary_errors 'mix'           [ retry-pass ]   2108
+rpl.rpl_trunc_temp 'stmt'                [ pass ]   2576
+main.information_schema                  [ pass ]  106092
+timer 5953: expired after 900 seconds
+worker[1] Trying to dump core for [mysqltest - pid: 5975, winpid: 5975]
+main.information_schema_all_engines      [ fail ]  timeout after 900 seconds
+        Test ended at 2009-06-18 18:37:25
+Retrying test, attempt(2/3)...
+
+***Warnings generated in error logs during shutdown after running tests: main.handler_myisam main.ctype_ujis_ucs2 main.ctype_recoding
+main.information_schema_chmod            [ pass ]     84
+rpl.rpl_circular_for_4_hosts 'stmt'      [ pass ]  344547
+timer 21612: expired after 21600 seconds
+Test suite timeout! Terminating...
+mysql-test-run: *** ERROR: Not all tests completed
+&quot;&quot;&quot;)
+        self.assertEqual(self.observer.progresses,
+                         map((lambda (x): ('tests', x)), [1,2,3,4,5,6,7,8]))
+        self.assertEqual(self.observer.testWarnLists,
+                         [[&quot;rpl.rpl_temporary_errors&quot;],
+                          [&quot;main.handler_myisam&quot;, &quot;main.ctype_ujis_ucs2&quot;, &quot;main.ctype_recoding&quot;]])
+        failtext1 = &quot;&quot;&quot;rpl.rpl_temporary_errors 'mix'        w2 [ fail ]
+        Test ended at 2009-06-18 16:21:28
+
+CURRENT_TEST: rpl.rpl_temporary_errors
+Retrying test, attempt(2/3)...
+
+&quot;&quot;&quot;
+        failtext2 = &quot;&quot;&quot;main.information_schema_all_engines      [ fail ]  timeout after 900 seconds
+        Test ended at 2009-06-18 18:37:25
+Retrying test, attempt(2/3)...
+
+&quot;&quot;&quot;
+        self.assertEqual(self.observer.testFails,
+                         [ (&quot;rpl.rpl_temporary_errors&quot;, &quot;mix&quot;, &quot;fail&quot;, &quot;&quot;, failtext1),
+                           (&quot;main.information_schema_all_engines&quot;, &quot;&quot;, &quot;fail&quot;, &quot;timeout after 900 seconds&quot;, failtext2)
+                           ])
+        self.assertEqual(self.observer.text[1:], [&quot;F:information_s...&quot;, &quot;F:rpl_temporary...&quot;, &quot;W:ctype_recoding&quot;, &quot;W:ctype_ujis_ucs2&quot;, &quot;W:handler_myisam&quot;])
+
 class RemoteShellTest(unittest.TestCase):
     def testRepr(self):
         # Test for #352
@@ -142,3 +271,47 @@ class RemoteShellTest(unittest.TestCase):
         testval = repr(rsc)
         rsc = buildstep.RemoteShellCommand('.', 'make')
         testval = repr(rsc)
+
+
+class SubunitLogObserver(subunitlogobserver.SubunitLogObserver):
+    &quot;&quot;&quot;Subclassed to allow testing behaviour without a real buildstep.&quot;&quot;&quot;
+
+    def __init__(self):
+        subunitlogobserver.SubunitLogObserver.__init__(self)
+        self.testFails = []
+        self.testWarnLists = []
+        # We don't have a buildstep in self.step.
+        # So we'll just install ourself there, so we can check the call of
+        # setProgress().
+        # Same for self.step.step_status.setText()
+        self.step = self
+        self.step_status = self
+        self.progresses = []
+        self.text = []
+
+    def setProgress(self, type, value):
+        self.progresses.append((type, value))
+
+    def setText(self, text):
+        self.text = text
+
+
+class SubunitLogObserverTests(ObserverTestCase):
+    observer_cls = SubunitLogObserver
+
+    def test1(self):
+        self._logStdout(&quot;&quot;&quot;
+test: foo
+success: foo
+test: bar
+failure: bar [
+string
+]
+test: gam
+skip: gam
+test: quux
+xfail: quux
+&quot;&quot;&quot;)
+        self.assertEqual(self.observer.progresses,
+            [('tests', 1), ('tests', 2), ('tests failed', 1), ('tests', 3),
+             ('tests failed', 2), ('tests', 4)])</diff>
      <filename>buildbot/test/test_buildstep.py</filename>
    </modified>
    <modified>
      <diff>@@ -13,7 +13,7 @@ from twisted.spread import pb
 from twisted.web.server import Site
 from twisted.web.distrib import ResourcePublisher
 from buildbot.process.builder import Builder
-from buildbot.process.factory import BasicBuildFactory
+from buildbot.process.factory import BasicBuildFactory, ArgumentsInTheWrongPlace
 from buildbot.changes.pb import PBChangeSource
 from buildbot.changes.mail import SyncmailMaildirSource
 from buildbot.steps.source import CVS, Darcs
@@ -1209,6 +1209,28 @@ c['builders'] = [{'name':'builder1', 'slavename':'bot1',
                   'builddir':'workdir', 'factory':f1}]
 &quot;&quot;&quot;
 
+cfg1_bad = \
+&quot;&quot;&quot;
+from buildbot.process.factory import BuildFactory, s
+from buildbot.steps.shell import ShellCommand
+from buildbot.steps.source import Darcs
+from buildbot.buildslave import BuildSlave
+BuildmasterConfig = c = {}
+c['slaves'] = [BuildSlave('bot1', 'pw1')]
+c['schedulers'] = []
+c['slavePortnum'] = 9999
+f1 = BuildFactory([ShellCommand(command='echo yes'),
+                   s(ShellCommand, command='old-style'),
+                   ])
+# it should be this:
+#f1.addStep(ShellCommand(command='echo args'))
+# but an easy mistake is to do this:
+f1.addStep(ShellCommand(), command='echo args')
+# this test makes sure this error doesn't get ignored
+c['builders'] = [{'name':'builder1', 'slavename':'bot1',
+                  'builddir':'workdir', 'factory':f1}]
+&quot;&quot;&quot;
+
 class Factories(unittest.TestCase):
     def printExpecting(self, factory, args):
         factory_keys = factory[1].keys()
@@ -1231,6 +1253,7 @@ class Factories(unittest.TestCase):
                                'description': None,
                                'workdir': None,
                                'logfiles': {},
+                               'lazylogfiles': False,
                                'usePTY': &quot;slave-config&quot;,
                                })
         shell_args.update(kwargs)
@@ -1248,6 +1271,7 @@ class Factories(unittest.TestCase):
                       'baseURL': None,
                       'defaultBranch': None,
                       'logfiles': {},
+                      'lazylogfiles' : False,
                       }
         darcs_args.update(kwargs)
         self.failUnlessIdentical(factory[0], Darcs)
@@ -1270,6 +1294,10 @@ class Factories(unittest.TestCase):
         self.failUnlessExpectedShell(steps[3], defaults=False,
                                      command=&quot;echo old-style&quot;)
 
+    def testBadAddStepArguments(self):
+        m = BuildMaster(&quot;.&quot;)
+        self.failUnlessRaises(ArgumentsInTheWrongPlace, m.loadConfig, cfg1_bad)
+
     def _loop(self, orig):
         step_class, kwargs = orig.getStepFactory()
         newstep = step_class(**kwargs)</diff>
      <filename>buildbot/test/test_config.py</filename>
    </modified>
    <modified>
      <diff>@@ -10,6 +10,9 @@ from twisted.python import log, logfile
 from buildbot.test.runutils import SignalMixin
 import os
 
+from buildbot.test.runutils import RunMixin, rmtree
+from buildbot.changes import changes
+
 '''Testcases to verify that the --log-size and --log-count options to
 create-master and create-slave actually work.
 </diff>
      <filename>buildbot/test/test_limitlogs.py</filename>
    </modified>
    <modified>
      <diff>@@ -418,7 +418,7 @@ class Locks(RunMixin, unittest.TestCase):
                              [(&quot;start&quot;, 1), (&quot;done&quot;, 1),
                               (&quot;start&quot;, 2), (&quot;done&quot;, 2)])
 
-    def testLock1a(self):
+    def dont_testLock1a(self): ## disabled -- test itself is buggy
         # just like testLock1, but we reload the config file first, with a
         # change that causes full1b to be changed. This tickles a design bug
         # in which full1a and full1b wind up with distinct Lock instances.
@@ -455,7 +455,7 @@ class Locks(RunMixin, unittest.TestCase):
         self.failUnless(self.events[:2] == [(&quot;start&quot;, 1), (&quot;start&quot;, 2)] or
                         self.events[:2] == [(&quot;start&quot;, 2), (&quot;start&quot;, 1)])
 
-    def testLock3(self):
+    def dont_testLock3(self): ## disabled -- test fails sporadically
         # two builds run on separate slaves with master-scoped locks should
         # not overlap
         self.control.getBuilder(&quot;full1c&quot;).requestBuild(self.req1)
@@ -475,21 +475,22 @@ class Locks(RunMixin, unittest.TestCase):
                                            (&quot;start&quot;, 1), (&quot;done&quot;, 1)]
                         )
 
-    def testLock4(self):
-        self.control.getBuilder(&quot;full1a&quot;).requestBuild(self.req1)
-        self.control.getBuilder(&quot;full1c&quot;).requestBuild(self.req2)
-        self.control.getBuilder(&quot;full1d&quot;).requestBuild(self.req3)
-        d = defer.DeferredList([self.req1.waitUntilFinished(),
-                                self.req2.waitUntilFinished(),
-                                self.req3.waitUntilFinished()])
-        d.addCallback(self._testLock4_1)
-        return d
-
-    def _testLock4_1(self, res):
-        # full1a starts, then full1d starts (because they do not interfere).
-        # Once both are done, full1c can run.
-        self.failUnlessEqual(self.events,
-                             [(&quot;start&quot;, 1), (&quot;start&quot;, 3),
-                              (&quot;done&quot;, 1), (&quot;done&quot;, 3),
-                              (&quot;start&quot;, 2), (&quot;done&quot;, 2)])
+    # This test has been disabled due to flakeyness/intermittentness
+#    def testLock4(self):
+#        self.control.getBuilder(&quot;full1a&quot;).requestBuild(self.req1)
+#        self.control.getBuilder(&quot;full1c&quot;).requestBuild(self.req2)
+#        self.control.getBuilder(&quot;full1d&quot;).requestBuild(self.req3)
+#        d = defer.DeferredList([self.req1.waitUntilFinished(),
+#                                self.req2.waitUntilFinished(),
+#                                self.req3.waitUntilFinished()])
+#        d.addCallback(self._testLock4_1)
+#        return d
+#
+#    def _testLock4_1(self, res):
+#        # full1a starts, then full1d starts (because they do not interfere).
+#        # Once both are done, full1c can run.
+#        self.failUnlessEqual(self.events,
+#                             [(&quot;start&quot;, 1), (&quot;start&quot;, 3),
+#                              (&quot;done&quot;, 1), (&quot;done&quot;, 3),
+#                              (&quot;start&quot;, 2), (&quot;done&quot;, 2)])
 </diff>
      <filename>buildbot/test/test_locks.py</filename>
    </modified>
    <modified>
      <diff>@@ -30,7 +30,8 @@ c['slaves'] = [BuildSlave('bot1', 'sekrit')]
 c['schedulers'] = []
 c['builders'] = []
 c['builders'].append({'name':'quick', 'slavename':'bot1',
-                      'builddir': 'quickdir', 'factory': f1})
+                      'builddir': 'quickdir', 'factory': f1,
+                      'slavebuilddir': 'slavequickdir'})
 c['slavePortnum'] = 0
 &quot;&quot;&quot;
 
@@ -64,7 +65,7 @@ from buildbot.scheduler import Scheduler
 c['schedulers'] = [Scheduler('dummy', None, 0.1, ['dummy', 'dummy2'])]
 
 c['builders'].append({'name': 'dummy', 'slavename': 'bot1',
-                      'builddir': 'dummy', 'factory': f2})
+                      'factory': f2})
 c['builders'].append({'name': 'dummy2', 'slavename': 'bot1',
                       'builddir': 'dummy2', 'factory': f2})
 &quot;&quot;&quot;
@@ -72,8 +73,8 @@ c['builders'].append({'name': 'dummy2', 'slavename': 'bot1',
 config_2 = config_base + &quot;&quot;&quot;
 c['builders'] = [{'name': 'dummy', 'slavename': 'bot1',
                   'builddir': 'dummy1', 'factory': f2},
-                 {'name': 'testdummy', 'slavename': 'bot1',
-                  'builddir': 'dummy2', 'factory': f2, 'category': 'test'}]
+                 {'name': 'test dummy', 'slavename': 'bot1',
+                  'factory': f2, 'category': 'test'}]
 &quot;&quot;&quot;
 
 config_3 = config_2 + &quot;&quot;&quot;
@@ -86,7 +87,7 @@ c['builders'].append({'name': 'bdummy', 'slavename': 'bot1',
 
 config_4 = config_base + &quot;&quot;&quot;
 c['builders'] = [{'name': 'dummy', 'slavename': 'bot1',
-                  'builddir': 'dummy', 'factory': f2}]
+                  'slavebuilddir': 'sdummy', 'factory': f2}]
 &quot;&quot;&quot;
 
 config_4_newbasedir = config_4 + &quot;&quot;&quot;
@@ -244,9 +245,9 @@ class BuilderNames(unittest.TestCase):
         m.readConfig = True
 
         self.failUnlessEqual(s.getBuilderNames(),
-                             [&quot;dummy&quot;, &quot;testdummy&quot;, &quot;adummy&quot;, &quot;bdummy&quot;])
+                             [&quot;dummy&quot;, &quot;test dummy&quot;, &quot;adummy&quot;, &quot;bdummy&quot;])
         self.failUnlessEqual(s.getBuilderNames(categories=['test']),
-                             [&quot;testdummy&quot;, &quot;bdummy&quot;])
+                             [&quot;test dummy&quot;, &quot;bdummy&quot;])
 
 class Disconnect(RunMixin, unittest.TestCase):
 
@@ -263,7 +264,7 @@ class Disconnect(RunMixin, unittest.TestCase):
         m.readConfig = True
         m.startService()
 
-        self.failUnlessEqual(s.getBuilderNames(), [&quot;dummy&quot;, &quot;testdummy&quot;])
+        self.failUnlessEqual(s.getBuilderNames(), [&quot;dummy&quot;, &quot;test dummy&quot;])
         self.s1 = s1 = s.getBuilder(&quot;dummy&quot;)
         self.failUnlessEqual(s1.getName(), &quot;dummy&quot;)
         self.failUnlessEqual(s1.getState(), (&quot;offline&quot;, []))
@@ -531,7 +532,7 @@ class Disconnect2(RunMixin, unittest.TestCase):
         m.readConfig = True
         m.startService()
 
-        self.failUnlessEqual(s.getBuilderNames(), [&quot;dummy&quot;, &quot;testdummy&quot;])
+        self.failUnlessEqual(s.getBuilderNames(), [&quot;dummy&quot;, &quot;test dummy&quot;])
         self.s1 = s1 = s.getBuilder(&quot;dummy&quot;)
         self.failUnlessEqual(s1.getName(), &quot;dummy&quot;)
         self.failUnlessEqual(s1.getState(), (&quot;offline&quot;, []))
@@ -605,9 +606,10 @@ class Basedir(RunMixin, unittest.TestCase):
         self.bot = bot = self.slaves['bot1'].bot
         self.builder = builder = bot.builders.get(&quot;dummy&quot;)
         self.failUnless(builder)
-        self.failUnlessEqual(builder.builddir, &quot;dummy&quot;)
+        # slavebuilddir value.
+        self.failUnlessEqual(builder.builddir, &quot;sdummy&quot;)
         self.failUnlessEqual(builder.basedir,
-                             os.path.join(&quot;slavebase-bot1&quot;, &quot;dummy&quot;))
+                             os.path.join(&quot;slavebase-bot1&quot;, &quot;sdummy&quot;))
 
         d = self.master.loadConfig(config_4_newbasedir)
         d.addCallback(self._testChangeBuilddir_2)
@@ -716,14 +718,15 @@ c['builders'] = [{'name': 'triggerer', 'slavename': 'bot1',
         self.assertEqual(self.getFlag(&quot;props&quot;), &quot;lit:dyn&quot;)
 
 class PropertyPropagation(RunMixin, TestFlagMixin, unittest.TestCase):
-    def setupTest(self, config, builders, checkFn):
+    def setupTest(self, config, builders, checkFn, changeProps={}):
         self.clearFlags()
         m = self.master
         m.loadConfig(config)
         m.readConfig = True
         m.startService()
 
-        c = changes.Change(&quot;bob&quot;, [&quot;Makefile&quot;, &quot;foo/bar.c&quot;], &quot;changed stuff&quot;)
+        c = changes.Change(&quot;bob&quot;, [&quot;Makefile&quot;, &quot;foo/bar.c&quot;], &quot;changed stuff&quot;,
+                           properties=changeProps)
         m.change_svc.addChange(c)
 
         d = self.connectSlave(builders=builders)
@@ -759,6 +762,30 @@ c['builders'] = [{'name': 'flagcolor', 'slavename': 'bot1',
                 'color=red sched=mysched')
         return self.setupTest(self.config_schprop, ['flagcolor'], _check)
 
+    config_changeprop = config_base + &quot;&quot;&quot;
+from buildbot.scheduler import Scheduler
+from buildbot.steps.dummy import Dummy
+from buildbot.test.runutils import SetTestFlagStep
+from buildbot.process.properties import WithProperties
+c['schedulers'] = [
+    Scheduler('mysched', None, 0.1, ['flagcolor'], properties={'color':'red'}),
+]
+factory = factory.BuildFactory([
+    s(SetTestFlagStep, flagname='testresult', 
+      value=WithProperties('color=%(color)s sched=%(scheduler)s prop1=%(prop1)s')),
+    ])
+c['builders'] = [{'name': 'flagcolor', 'slavename': 'bot1',
+                  'builddir': 'test', 'factory': factory},
+                ]
+&quot;&quot;&quot;
+
+    def testChangeProp(self):
+        def _check(res):
+            self.failUnlessEqual(self.getFlag('testresult'),
+                'color=blue sched=mysched prop1=prop1')
+        return self.setupTest(self.config_changeprop, ['flagcolor'], _check,
+                              changeProps={'color': 'blue', 'prop1': 'prop1'})
+
     config_slaveprop = config_base + &quot;&quot;&quot;
 from buildbot.scheduler import Scheduler
 from buildbot.steps.dummy import Dummy</diff>
      <filename>buildbot/test/test_run.py</filename>
    </modified>
    <modified>
      <diff>@@ -108,7 +108,7 @@ class Scheduling(unittest.TestCase):
         s.addChange(c3)
         
         self.failUnlessEqual(s.importantChanges, [c1,c3])
-        self.failUnlessEqual(s.unimportantChanges, [c2])
+        self.failUnlessEqual(s.allChanges, [c1,c2,c3])
         self.failUnless(s.timer)
 
         d = defer.Deferred()</diff>
      <filename>buildbot/test/test_scheduler.py</filename>
    </modified>
    <modified>
      <diff>@@ -391,6 +391,48 @@ class Mail(unittest.TestCase):
         self.assertFalse(mailer._shouldAttachLog('anything'))
         self.assertTrue(mailer._shouldAttachLog('something'))
 
+    def testShouldAttachPatches(self):
+        basedir = &quot;test_should_attach_patches&quot;
+        os.mkdir(basedir)
+        b1 = self.makeBuild(4, builder.FAILURE)
+        b1.setProperty('buildnumber', 1, 'Build')
+        b1.setText([&quot;snarkleack&quot;, &quot;polarization&quot;, &quot;failed&quot;])
+        b1.blamelist = [&quot;dev3&quot;, &quot;dev3&quot;, &quot;dev3&quot;, &quot;dev4&quot;,
+                        &quot;Thomas_Walters&quot;]
+        b1.source.changes = (Change(who = 'author1', files = ['file1'], comments = 'comment1', revision = 123),
+                             Change(who = 'author2', files = ['file2'], comments = 'comment2', revision = 456))
+        b1.testlogs = [MyLog(basedir, 'compile', &quot;Compile log here\n&quot;),
+                       MyLog(basedir, 'test', &quot;Test log here\nTest 1 failed\nTest 2 failed\nTest 3 failed\nTest 4 failed\n&quot;)]
+        b1.source.patch = (0, '--- /dev/null\n+++ a_file\n', None)
+
+        mailer = MyMailer(fromaddr=&quot;buildbot@example.com&quot;, addPatch=True)
+        mailer.parent = self
+        mailer.status = self
+        self.messages = []
+        mailer.buildFinished(&quot;builder1&quot;, b1, b1.results)
+        m,r = self.messages.pop()
+        self.assertTrue(m.is_multipart())
+        self.assertEqual(len([True for i in m.walk()]), 3)
+
+        mailer = MyMailer(fromaddr=&quot;buildbot@example.com&quot;, addPatch=False)
+        mailer.parent = self
+        mailer.status = self
+        self.messages = []
+        mailer.buildFinished(&quot;builder1&quot;, b1, b1.results)
+        m,r = self.messages.pop()
+        self.assertFalse(m.is_multipart())
+        self.assertEqual(len([True for i in m.walk()]), 1)
+
+        mailer = MyMailer(fromaddr=&quot;buildbot@example.com&quot;)
+        mailer.parent = self
+        mailer.status = self
+        self.messages = []
+        mailer.buildFinished(&quot;builder1&quot;, b1, b1.results)
+        m,r = self.messages.pop()
+        self.assertTrue(m.is_multipart())
+        self.assertEqual(len([True for i in m.walk()]), 3)
+
+
     def testFailure(self):
         mailer = MyMailer(fromaddr=&quot;buildbot@example.com&quot;, mode=&quot;problem&quot;,
                           extraRecipients=[&quot;recip@example.com&quot;,
@@ -899,8 +941,26 @@ class Log(unittest.TestCase):
         return d
     testLargeSummary.timeout = 5
 
+    def testLimit(self):
+        l = MyLog(self.basedir, &quot;limit&quot;)
+        l.logMaxSize = 150
+        for i in range(1000):
+            l.addStdout(&quot;Some data&quot;)
+        l.finish()
+        t = l.getText()
+        # Compare against 175 since we truncate logs based on chunks, so we may
+        # go slightly over the limit
+        self.failIf(len(t) &gt; 175, &quot;Text too long (%i)&quot; % len(t))
+        self.failUnless(&quot;truncated&quot; in l.getTextWithHeaders(),
+                &quot;No truncated message found&quot;)
 
 class CompressLog(unittest.TestCase):
+    # compression is not supported unless bz2 is installed
+    try:
+        import bz2
+    except:
+        skip = &quot;compression not supported (no bz2 module available)&quot;
+
     def testCompressLogs(self):
         bss = setupBuildStepStatus(&quot;test-compress&quot;)
         bss.build.builder.setLogCompressionLimit(1024)
@@ -1320,8 +1380,10 @@ class ContactTester(unittest.TestCase):
         self.failUnlessEqual(irc.message, &quot;&quot;, &quot;No started notification with notify_events=['failed']&quot;)
 
         irc.message = &quot;&quot;
+        irc.channel.showBlameList = True
         irc.buildFinished(my_builder.getName(), my_build, None)
-        self.failUnlessEqual(irc.message, &quot;build #862 of builder834 is complete: Failure [step1 step2]  Build details are at http://myserver/mypath?build=765&quot;, &quot;Finish notification generated on failure with notify_events=['failed']&quot;)
+        self.failUnlessEqual(irc.message, &quot;build #862 of builder834 is complete: Failure [step1 step2]  Build details are at http://myserver/mypath?build=765  blamelist: author1&quot;, &quot;Finish notification generated on failure with notify_events=['failed']&quot;)
+        irc.channel.showBlameList = False
 
         irc.message = &quot;&quot;
         my_build.results = builder.SUCCESS
@@ -1353,6 +1415,12 @@ class ContactTester(unittest.TestCase):
         self.failUnlessEqual(irc.message, &quot;build #862 of builder834 is complete: Exception [step1 step2]  Build details are at http://myserver/mypath?build=765&quot;, &quot;Finish notification generated on failure with notify_events=['exception']&quot;)
 
         irc.message = &quot;&quot;
+        irc.channel.showBlameList = True
+        irc.buildFinished(my_builder.getName(), my_build, None)
+        self.failUnlessEqual(irc.message, &quot;build #862 of builder834 is complete: Exception [step1 step2]  Build details are at http://myserver/mypath?build=765  blamelist: author1&quot;, &quot;Finish notification generated on failure with notify_events=['exception']&quot;)
+        irc.channel.showBlameList = False
+
+        irc.message = &quot;&quot;
         my_build.results = builder.SUCCESS
         irc.buildFinished(my_builder.getName(), my_build, None)
         self.failUnlessEqual(irc.message, &quot;&quot;, &quot;No finish notification generated on success with notify_events=['exception']&quot;)
@@ -1591,6 +1659,7 @@ class MyChannel:
 
     def __init__(self, notify_events = {}):
         self.notify_events = notify_events
+        self.showBlameList = False
 
 class MyContact(words.Contact):
     message = &quot;&quot;
@@ -1608,6 +1677,31 @@ class MyContact(words.Contact):
     def send(self, msg):
         self.message += msg
 
+class MyIrcStatusBot(words.IrcStatusBot):
+    def msg(self, dest, message):
+        self.message = ['msg', dest, message]
+
+    def notice(self, dest, message):
+        self.message = ['notice', dest, message]
+
+class IrcStatusBotTester(unittest.TestCase):
+    def testMsgOrNotice(self):
+        channel = MyIrcStatusBot('alice', 'pa55w0od', ['#here'],
+                                 builder.SUCCESS, None, {})
+        channel.msgOrNotice('bob', 'hello')
+        self.failUnlessEqual(channel.message, ['msg', 'bob', 'hello'])
+
+        channel.msgOrNotice('#here', 'hello')
+        self.failUnlessEqual(channel.message, ['msg', '#here', 'hello'])
+
+        channel.noticeOnChannel = True
+
+        channel.msgOrNotice('bob', 'hello')
+        self.failUnlessEqual(channel.message, ['msg', 'bob', 'hello'])
+
+        channel.msgOrNotice('#here', 'hello')
+        self.failUnlessEqual(channel.message, ['notice', '#here', 'hello'])
+
 class StepStatistics(unittest.TestCase):
     def testStepStatistics(self):
         status = builder.BuildStatus(builder.BuilderStatus(&quot;test&quot;), 123)</diff>
      <filename>buildbot/test/test_status.py</filename>
    </modified>
    <modified>
      <diff>@@ -14,6 +14,7 @@
 # todo: test batched updates, by invoking remote_update(updates) instead of
 # statusUpdate(update). Also involves interrupted builds.
 
+import sys
 import os
 
 from twisted.trial import unittest
@@ -21,6 +22,7 @@ from twisted.internet import reactor, defer
 
 from buildbot.sourcestamp import SourceStamp
 from buildbot.process import buildstep, base, factory
+from buildbot.process.properties import Properties, WithProperties
 from buildbot.buildslave import BuildSlave
 from buildbot.steps import shell, source, python, master
 from buildbot.status import builder
@@ -134,6 +136,7 @@ class BuildStep(unittest.TestCase):
                                  'want_stderr': 1,
                                  'logfiles': {},
                                  'timeout': 10,
+                                 'maxTime': None,
                                  'usePTY': 'slave-config',
                                  'env': None}) ] )
         self.assertEqual(self.remote.events, self.expectedEvents)
@@ -622,6 +625,112 @@ ending line
         results = step.evaluateCommand(cmd)
         self.failUnlessEqual(results, WARNINGS)
 
+    def testCompile4(self):
+        # Test suppression of warnings.
+        self.masterbase = &quot;Warnings.testCompile4&quot;
+        step = self.makeStep(shell.Compile,
+                             warningPattern=&quot;^(.*?):([0-9]+): [Ww]arning: (.*)$&quot;,
+                             warningExtractor=shell.Compile.warnExtractFromRegexpGroups,
+                             directoryEnterPattern=&quot;make.*: Entering directory [\&quot;`'](.*)['`\&quot;]&quot;,
+                             directoryLeavePattern=&quot;make.*: Leaving directory&quot;)
+        step.addSuppression([(r&quot;/subdir/&quot;, r&quot;xyzzy&quot;, None, None),
+                             (r&quot;foo.c&quot;, r&quot;.*&quot;, None, 20),
+                             (r&quot;foo.c&quot;, r&quot;.*&quot;, 200, None),
+                             (r&quot;foo.c&quot;, r&quot;.*&quot;, 50, 50),
+                             (r&quot;xxx&quot;, r&quot;.*&quot;, None, None),
+                             ])
+        log = step.addLog(&quot;stdio&quot;)
+        output = \
+&quot;&quot;&quot;Making all in .
+make[1]: Entering directory `/abs/path/build'
+foo.c:10: warning: `bar' defined but not used
+foo.c:50: warning: `bar' defined but not used
+make[2]: Entering directory `/abs/path/build/subdir'
+baz.c:33: warning: `xyzzy' defined but not used
+baz.c:34: warning: `magic' defined but not used
+make[2]: Leaving directory `/abs/path/build/subdir'
+foo.c:100: warning: `xyzzy' defined but not used
+foo.c:200: warning: `bar' defined but not used
+make[2]: Leaving directory `/abs/path/build'
+&quot;&quot;&quot;
+        log.addStdout(output)
+        log.finish()
+        step.createSummary(log)
+        self.failUnlessEqual(step.getProperty(&quot;warnings-count&quot;), 2)
+        logs = {}
+        for log in step.step_status.getLogs():
+            logs[log.getName()] = log
+        self.failUnless(&quot;warnings&quot; in logs)
+        lines = logs[&quot;warnings&quot;].readlines()
+        self.failUnlessEqual(len(lines), 2)
+        self.failUnlessEqual(lines[0], &quot;baz.c:34: warning: `magic' defined but not used\n&quot;)
+        self.failUnlessEqual(lines[1], &quot;foo.c:100: warning: `xyzzy' defined but not used\n&quot;)
+
+        cmd = buildstep.RemoteCommand(None, {})
+        cmd.rc = 0
+        results = step.evaluateCommand(cmd)
+        self.failUnlessEqual(results, WARNINGS)
+
+    def filterArgs(self, args):
+        if &quot;writer&quot; in args:
+            args[&quot;writer&quot;] = self.wrap(args[&quot;writer&quot;])
+        return args
+
+    suppressionFileData = &quot;&quot;&quot;
+# Sample suppressions file for testing
+
+/subdir/ : xyzzy
+foo.c: .* : 0-20
+foo.c: .*: 200-10000
+foo.c :.*: 50
+xxx : .*
+&quot;&quot;&quot;
+    def testCompile5(self):
+        # Test downloading warning suppression file from slave.
+        self.slavebase = &quot;Warnings.testCompile5.slave&quot;
+        self.masterbase = &quot;Warnings.testCompile5.master&quot;
+        sb = self.makeSlaveBuilder()
+        os.mkdir(os.path.join(self.slavebase, self.slavebuilderbase,
+                              &quot;build&quot;))
+        output = \
+&quot;&quot;&quot;Making all in .
+make[1]: Entering directory `/abs/path/build'
+foo.c:10: warning: `bar' defined but not used
+foo.c:50: warning: `bar' defined but not used
+make[2]: Entering directory `/abs/path/build/subdir'
+baz.c:33: warning: `xyzzy' defined but not used
+baz.c:34: warning: `magic' defined but not used
+make[2]: Leaving directory `/abs/path/build/subdir'
+foo.c:100: warning: `xyzzy' defined but not used
+foo.c:200: warning: `bar' defined but not used
+make[2]: Leaving directory `/abs/path/build'
+&quot;&quot;&quot;
+        printStatement = ('print &quot;&quot;&quot;%s&quot;&quot;&quot;' % output)
+        step = self.makeStep(shell.Compile,
+                             warningPattern=&quot;^(.*?):([0-9]+): [Ww]arning: (.*)$&quot;,
+                             warningExtractor=shell.Compile.warnExtractFromRegexpGroups,
+                             suppressionFile=&quot;warnings.supp&quot;,
+                             command=[sys.executable, &quot;-c&quot;, printStatement])
+        slavesrc = os.path.join(self.slavebase,
+                                self.slavebuilderbase,
+                                &quot;build&quot;,
+                                &quot;warnings.supp&quot;)
+        open(slavesrc, &quot;w&quot;).write(self.suppressionFileData)
+
+        d = self.runStep(step)
+        def _checkResult(result):
+            self.failUnlessEqual(step.getProperty(&quot;warnings-count&quot;), 2)
+            logs = {}
+            for log in step.step_status.getLogs():
+                logs[log.getName()] = log
+            self.failUnless(&quot;warnings&quot; in logs)
+            lines = logs[&quot;warnings&quot;].readlines()
+            self.failUnlessEqual(len(lines), 2)
+            self.failUnlessEqual(lines[0], &quot;baz.c:34: warning: `magic' defined but not used\n&quot;)
+            self.failUnlessEqual(lines[1], &quot;foo.c:100: warning: `xyzzy' defined but not used\n&quot;)
+
+        d.addCallback(_checkResult)
+        return d
 
 class TreeSize(StepTester, unittest.TestCase):
     def testTreeSize(self):
@@ -752,7 +861,9 @@ class MasterShellCommand(StepTester, unittest.TestCase):
         self.slavebase = &quot;testMasterShellCommand.slave&quot;
         self.masterbase = &quot;testMasterShellCommand.master&quot;
         sb = self.makeSlaveBuilder()
-        step = self.makeStep(master.MasterShellCommand, command=['echo', 'hi'])
+        step = self.makeStep(master.MasterShellCommand, command=['echo',
+                                   WithProperties(&quot;hi build-%(other)s.tar.gz&quot;)])
+        step.build.setProperty(&quot;other&quot;, &quot;foo&quot;, &quot;test&quot;)
 
         # we can't invoke runStep until the reactor is started .. hence this
         # little dance
@@ -764,7 +875,7 @@ class MasterShellCommand(StepTester, unittest.TestCase):
         def _check(results):
             self.failUnlessEqual(results, SUCCESS)
             logtxt = step.getLog(&quot;stdio&quot;).getText()
-            self.failUnlessEqual(logtxt.strip(), &quot;hi&quot;)
+            self.failUnlessEqual(logtxt.strip(), &quot;hi build-foo.tar.gz&quot;)
         d.addCallback(_check)
         reactor.callLater(0, d.callback, None)
         return d</diff>
      <filename>buildbot/test/test_steps.py</filename>
    </modified>
    <modified>
      <diff>@@ -90,6 +90,15 @@ diff -u -r1.1.1.1 subdir.c
  }
 &quot;&quot;&quot;
 
+# Test buildbot try --root support.
+subdir_diff = p0_diff.replace('subdir/subdir.c', 'subdir.c')
+
+# Test --patchlevel support.
+p2_diff = p0_diff.replace('subdir/subdir.c', 'foo/bar/subdir/subdir.c')
+
+# Used in do_patch() test.
+PATCHLEVEL0, SUBDIR_ROOT, PATCHLEVEL2 = range(3)
+
 # this patch does not include the filename headers, so it is
 # patchlevel-neutral
 TRY_PATCH = '''
@@ -286,7 +295,8 @@ class BaseHelper:
         # you must call this from createRepository
         self.repbase = os.path.abspath(os.path.join(&quot;test_vc&quot;,
                                                     &quot;repositories&quot;))
-        _makedirsif(self.repbase)
+        rmdirRecursive(self.repbase)
+        os.makedirs(self.repbase)
             
     def createRepository(self):
         # this will only be called once per process
@@ -725,7 +735,7 @@ class VCBase(SignalMixin):
         #self.checkGotRevisionIsLatest(bs)
         # VC 'export' is not required to have a got_revision
 
-    def do_patch(self):
+    def do_patch(self, type):
         vctype = self.vctype
         args = self.helper.vcargs
         m = self.master
@@ -736,12 +746,19 @@ class VCBase(SignalMixin):
             s += &quot;, %s=%s&quot; % (k, repr(v))
         s += &quot;)&quot;
         self.config = config_vc % s
-
+        if type == PATCHLEVEL0:
+          self.patch = (0, p0_diff)
+        elif type == SUBDIR_ROOT:
+          self.patch = (0, subdir_diff, 'subdir')
+        elif type == PATCHLEVEL2:
+          self.patch = (2, p2_diff)
+        else:
+          raise NotImplementedError
         m.loadConfig(self.config % &quot;clobber&quot;)
         m.readConfig = True
         m.startService()
 
-        ss = SourceStamp(revision=self.helper.trunk[-1], patch=(0, p0_diff))
+        ss = SourceStamp(revision=self.helper.trunk[-1], patch=self.patch)
 
         d = self.connectSlave()
         d.addCallback(lambda res: self.doBuild(ss=ss))
@@ -782,7 +799,7 @@ class VCBase(SignalMixin):
         return self._doPatch_3()
 
     def _doPatch_3(self, res=None):
-        ss = SourceStamp(revision=self.helper.trunk[-2], patch=(0, p0_diff))
+        ss = SourceStamp(revision=self.helper.trunk[-2], patch=self.patch)
         d = self.doBuild(ss=ss)
         d.addCallback(self._doPatch_4)
         return d
@@ -801,7 +818,7 @@ class VCBase(SignalMixin):
         # now check that we can patch a branch
         ss = SourceStamp(branch=self.helper.branchname,
                          revision=self.helper.branch[-1],
-                         patch=(0, p0_diff))
+                         patch=self.patch)
         d = self.doBuild(ss=ss)
         d.addCallback(self._doPatch_5)
         return d
@@ -1040,6 +1057,8 @@ class VCBase(SignalMixin):
 
 
     def dumpPatch(self, patch):
+        # FIXME: This function is never called.
+        raise NotImplementedError
         # this exists to help me figure out the right 'patchlevel' value
         # should be returned by tryclient.getSourceStamp
         n = self.mktemp()
@@ -1211,7 +1230,15 @@ class CVS(VCBase, unittest.TestCase):
         return d
 
     def testPatch(self):
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):
@@ -1418,7 +1445,21 @@ class SVN(VCBase, unittest.TestCase):
         self.helper.vcargs = { 'baseURL': self.helper.svnurl + &quot;/&quot;,
                                'defaultBranch': &quot;sample/trunk&quot;,
                                }
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = { 'baseURL': self.helper.svnurl + &quot;/&quot;,
+                               'defaultBranch': &quot;sample/trunk&quot;,
+                               }
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = { 'baseURL': self.helper.svnurl + &quot;/&quot;,
+                               'defaultBranch': &quot;sample/trunk&quot;,
+                               }
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):
@@ -1607,7 +1648,21 @@ class P4(VCBase, unittest.TestCase):
         self.helper.vcargs = { 'p4port': self.helper.p4port,
                                'p4base': '//depot/',
                                'defaultBranch': 'trunk' }
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = { 'p4port': self.helper.p4port,
+                               'p4base': '//depot/',
+                               'defaultBranch': 'trunk' }
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = { 'p4port': self.helper.p4port,
+                               'p4base': '//depot/',
+                               'defaultBranch': 'trunk' }
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
 VCS.registerVC(P4.vc_name, P4Helper())
@@ -1724,7 +1779,19 @@ class Darcs(VCBase, unittest.TestCase):
     def testPatch(self):
         self.helper.vcargs = { 'baseURL': self.helper.darcs_base + &quot;/&quot;,
                                'defaultBranch': &quot;trunk&quot; }
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = { 'baseURL': self.helper.darcs_base + &quot;/&quot;,
+                               'defaultBranch': &quot;trunk&quot; }
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = { 'baseURL': self.helper.darcs_base + &quot;/&quot;,
+                               'defaultBranch': &quot;trunk&quot; }
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):
@@ -2026,7 +2093,19 @@ class Arch(VCBase, unittest.TestCase):
     def testPatch(self):
         self.helper.vcargs = {'url': self.helper.archrep,
                               'version': self.helper.defaultbranch }
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = {'url': self.helper.archrep,
+                              'version': self.helper.defaultbranch }
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = {'url': self.helper.archrep,
+                              'version': self.helper.defaultbranch }
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):
@@ -2104,7 +2183,25 @@ class Bazaar(Arch):
                               'archive': self.helper.archname,
                               'version': self.helper.defaultbranch,
                               }
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = {'url': self.helper.archrep,
+                              # Baz adds the required 'archive' argument
+                              'archive': self.helper.archname,
+                              'version': self.helper.defaultbranch,
+                              }
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = {'url': self.helper.archrep,
+                              # Baz adds the required 'archive' argument
+                              'archive': self.helper.archname,
+                              'version': self.helper.defaultbranch,
+                              }
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):
@@ -2292,7 +2389,7 @@ class BzrHelper(BaseHelper):
             rep = self.rep_trunk
         else:
             rep = os.path.join(self.bzr_base, branch)
-        w = self.dovc(self.bzr_base, [&quot;checkout&quot;, rep, workdir])
+        w = self.dovc(self.bzr_base, [&quot;clone&quot;, rep, workdir])
         yield w; w.getResult()
         open(os.path.join(workdir, &quot;subdir&quot;, &quot;subdir.c&quot;), &quot;w&quot;).write(TRY_C)
     vc_try_checkout = deferredGenerator(vc_try_checkout)
@@ -2321,11 +2418,25 @@ class Bzr(VCBase, unittest.TestCase):
         # TODO: testRetry has the same problem with Bzr as it does for
         # Arch
         return d
+    # Bzr is *slow*, and the testCheckout step takes a *very* long time
+    testCheckout.timeout = 480
 
     def testPatch(self):
         self.helper.vcargs = { 'baseURL': self.helper.bzr_base + &quot;/&quot;,
                                'defaultBranch': &quot;trunk&quot; }
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = { 'baseURL': self.helper.bzr_base + &quot;/&quot;,
+                               'defaultBranch': &quot;trunk&quot; }
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = { 'baseURL': self.helper.bzr_base + &quot;/&quot;,
+                               'defaultBranch': &quot;trunk&quot; }
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):
@@ -2439,7 +2550,7 @@ class MercurialHelper(BaseHelper):
         yield w; w.getResult()
         w = self.dovc(tmp, &quot;add&quot;)
         yield w; w.getResult()
-        w = self.dovc(tmp, ['commit', '-m', 'initial_import'])
+        w = self.dovc(tmp, ['commit', '-m', 'initial_import', '--user' ,'bbtests@localhost' ])
         yield w; w.getResult()
         w = self.dovc(tmp, ['push', self.rep_trunk])
         # note that hg-push does not actually update the working directory
@@ -2449,7 +2560,7 @@ class MercurialHelper(BaseHelper):
         self.addTrunkRev(self.extract_id(out))
 
         self.populate_branch(tmp)
-        w = self.dovc(tmp, ['commit', '-m', 'commit_on_branch'])
+        w = self.dovc(tmp, ['commit', '-m', 'commit_on_branch', '--user' ,'bbtests@localhost' ])
         yield w; w.getResult()
         w = self.dovc(tmp, ['push', self.rep_branch])
         yield w; w.getResult()
@@ -2472,7 +2583,7 @@ class MercurialHelper(BaseHelper):
         # force the mtime forward a little bit
         future = time.time() + 2*self.version
         os.utime(version_c_filename, (future, future))
-        w = self.dovc(tmp, ['commit', '-m', 'revised_to_%d' % self.version])
+        w = self.dovc(tmp, ['commit', '-m', 'revised_to_%d' % self.version, '--user' ,'bbtests@localhost' ])
         yield w; w.getResult()
         w = self.dovc(tmp, ['push', self.rep_trunk])
         yield w; w.getResult()
@@ -2545,7 +2656,19 @@ class Mercurial(VCBase, unittest.TestCase):
     def testPatch(self):
         self.helper.vcargs = { 'baseURL': self.helper.hg_base + &quot;/&quot;,
                                'defaultBranch': &quot;trunk&quot; }
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = { 'baseURL': self.helper.hg_base + &quot;/&quot;,
+                               'defaultBranch': &quot;trunk&quot; }
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = { 'baseURL': self.helper.hg_base + &quot;/&quot;,
+                               'defaultBranch': &quot;trunk&quot; }
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):
@@ -2741,7 +2864,17 @@ class MercurialInRepo(Mercurial):
 
     def testPatch(self):
         self.helper.vcargs = self.default_args()
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = self.default_args()
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = self.default_args()
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):
@@ -2880,6 +3013,12 @@ class GitHelper(BaseHelper):
         w = self.dovc(self.repbase, &quot;init&quot;, env=env)
         yield w; w.getResult()
 
+        # NetBSD pkgsrc uses templates that stupidly enable the update hook, requiring
+        # a non-default description.  This is broken, but easily worked around.
+        # http://www.netbsd.org/cgi-bin/query-pr-single.pl?number=41683
+        descrfile = os.path.join(self.gitrepo, &quot;description&quot;)
+        open(descrfile, &quot;w&quot;).write(&quot;NetBSD workaround&quot;)
+
         self.populate(tmp)
         w = self.dovc(tmp, &quot;init&quot;)
         yield w; w.getResult()
@@ -2983,7 +3122,19 @@ class Git(VCBase, unittest.TestCase):
     def testPatch(self):
         self.helper.vcargs = { 'repourl': self.helper.gitrepo,
                                'branch': &quot;master&quot; }
-        d = self.do_patch()
+        d = self.do_patch(PATCHLEVEL0)
+        return d
+
+    def testPatchSubDir(self):
+        self.helper.vcargs = { 'repourl': self.helper.gitrepo,
+                               'branch': &quot;master&quot; }
+        d = self.do_patch(SUBDIR_ROOT)
+        return d
+
+    def testPatchP2(self):
+        self.helper.vcargs = { 'repourl': self.helper.gitrepo,
+                               'branch': &quot;master&quot; }
+        d = self.do_patch(PATCHLEVEL2)
         return d
 
     def testCheckoutBranch(self):</diff>
      <filename>buildbot/test/test_vc.py</filename>
    </modified>
    <modified>
      <diff>@@ -5,7 +5,7 @@ github_buildbot.py is based on git_buildbot.py
 github_buildbot.py will determine the repository information from the JSON 
 HTTP POST it receives from github.com and build the appropriate repository.
 If your github repository is private, you must add a ssh key to the github
-repository for the user who initiated the buildslave.
+repository for the user who initiated the build on the buildslave.
 
 &quot;&quot;&quot;
 
@@ -55,7 +55,7 @@ class GitHubBuildBot(resource.Resource):
             for msg in traceback.format_exception(*sys.exc_info()):
                 logging.error(msg.strip())
 
-    def process_change(self, payload):
+    def process_change(self, payload, user, repo, github_url):
         &quot;&quot;&quot;
         Consumes the JSON as a python object and actually starts the build.
         
@@ -85,12 +85,15 @@ class GitHubBuildBot(resource.Resource):
                 files.extend(commit['modified'])
                 files.extend(commit['removed'])
                 change = {'revision': commit['id'],
+                     'revlink': commit['url'],
                      'comments': commit['message'],
                      'branch': branch,
                      'who': commit['author']['name'] 
                             + &quot; &lt;&quot; + commit['author']['email'] + &quot;&gt;&quot;,
                      'files': files,
                      'links': [commit['url']],
+                     'properties': {'repository':
+                                    self.repo_url(user, repo, github_url)},
                 }
                 changes.append(change)
         
@@ -152,7 +155,7 @@ def main():
         
     parser.add_option(&quot;-p&quot;, &quot;--port&quot;, 
         help=&quot;Port the HTTP server listens to for the GitHub Service Hook&quot;
-            + &quot; [default: %default]&quot;, default=4000, dest=&quot;port&quot;)
+            + &quot; [default: %default]&quot;, default=4000, type=int, dest=&quot;port&quot;)
         
     parser.add_option(&quot;-m&quot;, &quot;--buildmaster&quot;,
         help=&quot;Buildbot Master host and port. ie: localhost:9989 [default:&quot; 
@@ -169,7 +172,9 @@ def main():
             + &quot; %default]&quot;, default='warn', dest=&quot;level&quot;)
         
     parser.add_option(&quot;-g&quot;, &quot;--github&quot;, 
-        help=&quot;The github serve [default: %default]&quot;, default='github.com',
+        help=&quot;The github server.  Changing this is useful if you've specified&quot;      
+            + &quot;  a specific HOST handle in ~/.ssh/config for github &quot;   
+            + &quot;[default: %default]&quot;, default='github.com',
         dest=&quot;github&quot;)
         
     (options, _) = parser.parse_args()</diff>
      <filename>contrib/github_buildbot.py</filename>
    </modified>
    <modified>
      <diff>@@ -162,7 +162,7 @@ class ChangeSender:
         who = commands.getoutput('svnlook author %s &quot;%s&quot;' % (rev_arg, repo))
         revision = opts.get('revision')
         if revision is not None:
-            revision = int(revision)
+            revision = str(int(revision))
 
         # see if we even need to notify buildbot by looking at filters first
         changestring = '\n'.join(changed)</diff>
      <filename>contrib/svn_buildbot.py</filename>
    </modified>
    <modified>
      <diff>@@ -223,7 +223,9 @@ Simple ShellCommand Subclasses
 * Test::
 * TreeSize::
 * PerlModuleTest::
+* Testing with mysql-test-run::
 * SetProperty::
+* SubunitShellCommand::
 
 Python BuildSteps
 
@@ -1633,6 +1635,12 @@ branch='buildbot--usebranches--0', files=['buildbot/master.py']
 branch='warner-newfeature', files=['src/foo.c']
 @end table
 
+@heading Build Properties
+
+A Change may have one or more properties attached to it, usually specified
+through the Force Build form or @pxref{sendchange}. Properties are discussed
+in detail in the @pxref{Build Properties} section.
+
 @heading Links
 
 @c TODO: who is using 'links'? how is it being used?
@@ -1746,11 +1754,13 @@ SourceStamp, which will result in same behavior as this.
 This builds the most recent code on the given BRANCH. Again, this is
 generally triggered by a user request or Periodic build.
 
-@item (revision=REV, changes=None, patch=(LEVEL, DIFF))
+@item (revision=REV, changes=None, patch=(LEVEL, DIFF, SUBDIR_ROOT))
 This checks out the tree at the given revision REV, then applies a
-patch (using @code{patch -pLEVEL &lt;DIFF}). The @ref{try} feature uses
-this kind of @code{SourceStamp}. If @code{patch} is None, the patching
-step is bypassed.
+patch (using @code{patch -pLEVEL &lt;DIFF}) from inside the relative
+directory SUBDIR_ROOT. Item SUBDIR_ROOT is optional and defaults to the
+builder working directory. The @ref{try} feature uses this kind of
+@code{SourceStamp}. If @code{patch} is None, the patching step is
+bypassed.
 
 @end table
 
@@ -1836,7 +1846,7 @@ f.addStep(Compile())
 c['builders'] = [
   @{'name': 'test', 'slavenames': ['slave1', 'slave2', 'slave3', 'slave4',
                                    'slave5', 'slave6'],
-    'builddir': 'test', 'factory': f',
+    'builddir': 'test', 'factory': f', 'slavebuilddir': 'test',
     'env': @{'PATH': '/opt/local/bin:/opt/app/bin:/usr/local/bin:/usr/bin'@}@}
 
 @end example
@@ -1998,6 +2008,9 @@ These properties apply to all builds.
 @item schedulers --
 A scheduler can specify properties available to all the builds it
 starts.
+@item changes --
+A change can have properties attached to it. These are usually specified
+through Force Build or sendchange.
 @item buildslaves --
 A buildslave can pass properties on to the builds it performs.
 @item builds --
@@ -2255,6 +2268,20 @@ be a reasonable default on most file systems. This setting has no impact
 on status plugins, and merely affects the required disk space on the
 master for build logs.
 
+@bcindex c['logMaxSize']
+The @code{logMaxSize} parameter sets an upper limit (in bytes) to how large
+logs from an individual build step can be.  The default value is None, meaning
+no upper limit to the log size.  Any output exceeding @code{logMaxSize} will be
+truncated, and a message to this effect will be added to the log's HEADER
+channel.
+
+@bcindex c['logMaxTailSize']
+If @code{logMaxSize} is set, and the output from a step exceeds the maximum,
+the @code{logMaxTailSize} parameter controls how much of the end of the build
+log will be kept.  The effect of setting this parameter is that the log will
+contain the first @code{logMaxSize} bytes and the last @code{logMaxTailSize}
+bytes of output.  Don't set this value too high, as the the tail of the log is
+kept in memory.
 
 @node Change Sources and Schedulers, Merging BuildRequests, Defining the Project, Configuration
 @section Change Sources and Schedulers
@@ -2442,6 +2469,11 @@ A callable which takes one argument, a Change instance, and returns
 it is not.  Unimportant Changes are accumulated until the build is
 triggered by an important change.  The default value of None means
 that all Changes are important.
+
+@item categories
+A list of categories of changes that this scheduler will respond to.  If this
+is specified, then any non-matching changes are ignored.
+
 @end table
 
 @node Dependent Scheduler, Periodic Scheduler, AnyBranchScheduler, Change Sources and Schedulers
@@ -2482,7 +2514,7 @@ build, then those changes will be included in the downstream build.
 See the @pxref{Triggerable Scheduler} for a more flexible dependency
 mechanism that can avoid this problem.
 
-The arguments to this scheduler are:
+The keyword arguments to this scheduler are:
 
 @table @code
 @item name
@@ -2502,9 +2534,9 @@ Example:
 from buildbot import scheduler
 tests = scheduler.Scheduler(&quot;just-tests&quot;, None, 5*60,
                             [&quot;full-linux&quot;, &quot;full-netbsd&quot;, &quot;full-OSX&quot;])
-package = scheduler.Dependent(&quot;build-package&quot;,
-                              tests, # upstream scheduler -- no quotes!
-                              [&quot;make-tarball&quot;, &quot;make-deb&quot;, &quot;make-rpm&quot;])
+package = scheduler.Dependent(name=&quot;build-package&quot;,
+                              upstream=tests, # &lt;- no quotes!
+                              builderNames=[&quot;make-tarball&quot;, &quot;make-deb&quot;, &quot;make-rpm&quot;])
 c['schedulers'] = [tests, package]
 @end example
 
@@ -2700,7 +2732,7 @@ test = scheduler.Triggerable(name=&quot;distributed-test&quot;,
 package = scheduler.Triggerable(name=&quot;package-all-platforms&quot;,
                 builderNames=[&quot;package-all-platforms&quot;])
 
-c['schedulers'] = [checkin, nightly, build, test, package]
+c['schedulers'] = [mktarball, checkin, nightly, build, test, package]
 
 # on checkin, make a tarball, build it, and test it
 checkin_factory = factory.BuildFactory()
@@ -3293,12 +3325,6 @@ occurs on some of the buildslaves and not the others. Different
 platforms, operating systems, versions of major programs or libraries,
 all these things mean you should use separate Builders.
 
-@item builddir
-This specifies the name of a subdirectory (under the base directory)
-in which everything related to this builder will be placed. On the
-buildmaster, this holds build status information. On the buildslave,
-this is where checkouts, compiles, and tests are run.
-
 @item factory
 This is a @code{buildbot.process.factory.BuildFactory} instance which
 controls how the build is performed. Full details appear in their own
@@ -3312,6 +3338,22 @@ Other optional keys may be set on each Builder:
 
 @table @code
 
+@item builddir
+Specifies the name of a subdirectory (under the base directory) in which
+everything related to this builder will be placed on the buildmaster.
+This holds build status information. If not set, defaults to @code{name}
+with some characters escaped. Each builder must have a unique build
+directory.
+
+@item slavebuilddir
+Specifies the name of a subdirectory (under the base directory) in which
+everything related to this builder will be placed on the buildslave.
+This is where checkouts, compiles, and tests are run. If not set,
+defaults to @code{builddir}. If a slave is connected to multiple builders
+that shares the same @code{slavebuilddir}, make sure the slave is set to
+run one build at a time or ensure this is fine to run multiple builds from
+the same directory simultaneously.
+
 @item category
 If provided, this is a string that identifies a category for the
 builder to be a part of. Status clients can limit themselves to a
@@ -5655,10 +5697,19 @@ f.addStep(ShellCommand(
 @end example
 
 
+@item lazylogfiles
+If set to @code{True}, logfiles will be tracked lazily, meaning that they will
+only be added when and if something is written to them. This can be used to
+suppress the display of empty or missing log files. The default is @code{False}.
+
+
 @item timeout
 if the command fails to produce any output for this many seconds, it
 is assumed to be locked up and will be killed.
 
+@item maxTime
+if the command takes longer than this many seconds, it will be killed.
+
 @item description
 This will be used to describe the command (on the Waterfall display)
 while the command is still running. It should be a single
@@ -5705,7 +5756,9 @@ file less verbose.
 * Test::
 * TreeSize::
 * PerlModuleTest::
+* Testing with mysql-test-run::
 * SetProperty::
+* SubunitShellCommand::
 @end menu
 
 @node Configure, Compile, Simple ShellCommand Subclasses, Simple ShellCommand Subclasses
@@ -5748,6 +5801,66 @@ The @code{warningPattern=} can also be a pre-compiled python regexp
 object: this makes it possible to add flags like @code{re.I} (to use
 case-insensitive matching).
 
+The @code{suppressionFile=} argument can be specified as the (relative) path
+of a file inside the workdir defining warnings to be suppressed from the
+warning counting and log file. The file will be uploaded to the master from
+the slave before compiling, and any warning matched by a line in the
+suppression file will be ignored. This is useful to accept certain warnings
+(eg. in some special module of the source tree or in cases where the compiler
+is being particularly stupid), yet still be able to easily detect and fix the
+introduction of new warnings.
+
+The file must contain one line per pattern of warnings to ignore. Empty lines
+and lines beginning with @code{#} are ignored. Other lines must consist of a
+regexp matching the file name, followed by a colon (@code{:}), followed by a
+regexp matching the text of the warning. Optionally this may be followed by
+another colon and a line number range. For example:
+
+@example
+# Sample warning suppression file
+
+mi_packrec.c : .*result of 32-bit shift implicitly converted to 64 bits.* : 560-600
+DictTabInfo.cpp : .*invalid access to non-static.*
+kernel_types.h : .*only defines private constructors and has no friends.* : 51
+@end example
+
+If no line number range is specified, the pattern matches the whole file; if
+only one number is given it matches only on that line.
+
+The default warningPattern regexp only matches the warning text, so line
+numbers and file names are ignored. To enable line number and file name
+matching, privide a different regexp and provide a function (callable) as the
+argument of @code{warningExtractor=}. The function is called with three
+arguments: the BuildStep object, the line in the log file with the warning,
+and the @code{SRE_Match} object of the regexp search for @code{warningPattern}. It
+should return a tuple @code{(filename, linenumber, warning_test)}. For
+example:
+
+@example
+f.addStep(Compile(command=[&quot;make&quot;],
+                  warningPattern=&quot;^(.*?):([0-9]+): [Ww]arning: (.*)$&quot;,
+                  warningExtractor=Compile.warnExtractFromRegexpGroups,
+                  suppressionFile=&quot;support-files/compiler_warnings.supp&quot;))
+@end example
+
+(@code{Compile.warnExtractFromRegexpGroups} is a pre-defined function that
+returns the filename, linenumber, and text from groups (1,2,3) of the regexp
+match).
+
+In projects with source files in multiple directories, it is possible to get
+full path names for file names matched in the suppression file, as long as the
+build command outputs the names of directories as they are entered into and
+left again. For this, specify regexps for the arguments
+@code{directoryEnterPattern=} and @code{directoryLeavePattern=}. The
+@code{directoryEnterPattern=} regexp should return the name of the directory
+entered into in the first matched group. The defaults, which are suitable for
+GNU Make, are these:
+
+@example
+directoryEnterPattern = &quot;make.*: Entering directory [\&quot;`'](.*)['`\&quot;]&quot;
+directoryLeavePattern = &quot;make.*: Leaving directory&quot;
+@end example
+
 (TODO: this step needs to be extended to look for GCC error messages
 as well, and collect them into a separate logfile, along with the
 source code filenames involved).
@@ -5771,7 +5884,7 @@ of the code tree. It puts the size (as a count of 1024-byte blocks,
 aka 'KiB' or 'kibibytes') on the step's status text, and sets a build
 property named 'tree-size-KiB' with the same value.
 
-@node PerlModuleTest, SetProperty, TreeSize, Simple ShellCommand Subclasses
+@node PerlModuleTest, Testing with mysql-test-run, TreeSize, Simple ShellCommand Subclasses
 @subsubsection PerlModuleTest
 
 @bsindex buildbot.steps.shell.PerlModuleTest
@@ -5780,7 +5893,95 @@ This is a simple command that knows how to run tests of perl modules.
 It parses the output to determine the number of tests passed and
 failed and total number executed, saving the results for later query.
 
-@node SetProperty,  , PerlModuleTest, Simple ShellCommand Subclasses
+@node Testing with mysql-test-run, SetProperty, PerlModuleTest, Simple ShellCommand Subclasses
+@subsection Testing with mysql-test-run
+
+The @code{process.mtrlogobserver.MTR} class is a subclass of @code{Test}
+(@ref{Test}). It is used to run test suites using the mysql-test-run program,
+as used in MySQL, Drizzle, MariaDB, and MySQL storage engine plugins.
+
+The shell command to run the test suite is specified in the same way as for
+the Test class. The MTR class will parse the output of running the test suite,
+and use the count of tests executed so far to provide more accurate completion
+time estimates. Any test failures that occur during the test are summarized on
+the Waterfall Display.
+
+Server error logs are added as additional log files, useful to debug test
+failures.
+
+Optionally, data about the test run and any test failures can be inserted into
+a database for further analysis and report generation. To use this facility,
+create an instance of @code{twisted.enterprise.adbapi.ConnectionPool} with
+connections to the database. The necessary tables can be created automatically
+by setting @code{autoCreateTables} to @code{True}, or manually using the SQL
+found in the @file{mtrlogobserver.py} source file.
+
+One problem with specifying a database is that each reload of the
+configuration will get a new instance of @code{ConnectionPool} (even if the
+connection parameters are the same). To avoid that Buildbot thinks the builder
+configuration has changed because of this, use the
+@code{process.mtrlogobserver.EqConnectionPool} subclass of
+@code{ConnectionPool}, which implements an equiality operation that avoids
+this problem.
+
+Example use:
+
+@example
+from buildbot.process.mtrlogobserver import MTR, EqConnectionPool
+myPool = EqConnectionPool(&quot;MySQLdb&quot;, &quot;host&quot;, &quot;buildbot&quot;, &quot;password&quot;, &quot;db&quot;)
+myFactory.addStep(MTR(workdir=&quot;mysql-test&quot;, dbpool=myPool,
+                      command=[&quot;perl&quot;, &quot;mysql-test-run.pl&quot;, &quot;--force&quot;]))
+@end example
+
+@code{MTR} arguments:
+
+@table @code
+
+@item textLimit
+Maximum number of test failures to show on the waterfall page (to not flood
+the page in case of a large number of test failures. Defaults to 5.
+
+@item testNameLimit
+Maximum length of test names to show unabbreviated in the waterfall page, to
+avoid excessive column width. Defaults to 16.
+
+@item parallel
+Value of @code{--parallel} option used for mysql-test-run.pl (number of processes
+used to run the test suite in parallel). Defaults to 4. This is used to
+determine the number of server error log files to download from the
+slave. Specifying a too high value does not hurt (as nonexisting error logs
+will be ignored), however if using @code{--parallel} value greater than the default
+it needs to be specified, or some server error logs will be missing.
+
+@item dbpool
+An instance of twisted.enterprise.adbapi.ConnectionPool, or None.  Defaults to
+None. If specified, results are inserted into the database using the
+ConnectionPool.
+
+@item autoCreateTables
+Boolean, defaults to False. If True (and @code{dbpool} is specified), the
+necessary database tables will be created automatically if they do not exist
+already. Alternatively, the tables can be created manually from the SQL
+statements found in the mtrlogobserver.py source file.
+
+@item test_type
+Short string that will be inserted into the database in the row for the test
+run. Defaults to the empty string, but can be specified to identify different
+types of test runs.
+
+@item test_info
+Descriptive string that will be inserted into the database in the row for the test
+run. Defaults to the empty string, but can be specified as a user-readable
+description of this particular test run.
+
+@item mtr_subdir
+The subdirectory in which to look for server error log files. Defaults to
+``mysql-test'', which is usually correct. WithProperties is supported.
+
+@end table
+
+
+@node SetProperty, SubunitShellCommand , Testing with mysql-test-run, Simple ShellCommand Subclasses
 @subsubsection SetProperty
 
 @bsindex buildbot.steps.shell.SetProperty
@@ -5823,6 +6024,21 @@ f.addStep(SetProperty(
 Then @code{my_extract} will see @code{stdout=&quot;output1\noutput2\n&quot;}
 and @code{stderr=&quot;error\n&quot;}.
 
+@node SubunitShellCommand,  , SetProperty, Simple ShellCommand Subclasses
+@subsubsection SubunitShellCommand
+
+@bsindex buildbot.process.subunitlogger.SubunitShellCommand
+
+This buildstep is similar to ShellCommand, except that it runs the log content
+through a subunit filter to extract test and failure counts.
+
+@example
+f.addStep(SubunitShellCommand(command=&quot;make test&quot;))
+@end example
+
+This runs @code{make test} and filters it through subunit. The 'tests' and
+'test failed' progress metrics will now accumulate test data from the test run.
+
 @node Python BuildSteps, Transferring Files, Simple ShellCommand Subclasses, Build Steps
 @subsection Python BuildSteps
 
@@ -6641,7 +6857,7 @@ build number.
 @example
 class TestWithCodeCoverage(BuildStep):
     command = [&quot;make&quot;, &quot;test&quot;,
-               WithProperties(&quot;buildnum=%s&quot; % &quot;buildnumber&quot;)]
+               WithProperties(&quot;buildnum=%s&quot;, &quot;buildnumber&quot;)]
 
     def createSummary(self, log):
         buildnumber = self.getProperty(&quot;buildnumber&quot;)
@@ -6655,7 +6871,7 @@ output by the build process itself:
 @example
 class TestWithCodeCoverage(BuildStep):
     command = [&quot;make&quot;, &quot;test&quot;,
-               WithProperties(&quot;buildnum=%s&quot; % &quot;buildnumber&quot;)]
+               WithProperties(&quot;buildnum=%s&quot;, &quot;buildnumber&quot;)]
 
     def createSummary(self, log):
         output = StringIO(log.getText())
@@ -7615,6 +7831,40 @@ of the page, and the build hosts are listed across the top.  It accepts
 the same query arguments. The exception being that instead of ``width''
 the argument is named ``length.''
 
+@item /console
+
+EXPERIMENTAL: This provides a developer-oriented display of the the last
+changes and how they affected the builders.
+
+It allows a developer to quickly see the status of each builder for the
+first build including his or her change. A green box means that the change
+succeeded for all the steps for a given builder. A red box means that
+the changed introduced a new regression on a builder. An orange box
+means that at least one of the test failed, but it was also failing
+in the previous build, so it is not possible to see if there was any
+regressions from this change. Finally a yellow box means that the test
+is in progress.
+
+By adding one or more ``builder='' query arguments, the Console view is
+restricted to only showing information about the given Builders. By
+adding one or more ``branch='' query arguments, the display is
+restricted to showing information about the given branches. In
+addition, adding one or more ``category='' query arguments to the URL
+will limit the display to Builders that were defined with one of the
+given categories.
+
+By adding one or more ``name='' query arguments, the console view is
+restricted to only showing changes made by the given users.
+
+NOTE: To use this page, your buildbot.css file in public_html
+must be the one found in buildbot/status/web/extended.css.
+
+The console view is still in development. At this moment it supports
+only the source control managers that have an integer based revision id,
+like svn. It also has some issues with displaying multiple braches at the
+same time. If you do have multiple branches, you should use the
+``branch='' query argument.
+
 @item /rss
 
 This provides a rss feed summarizing all failed builds. The same
@@ -7956,6 +8206,11 @@ builders or categories, but not both.
 messages. These can be quite large. This can also be set to a list of
 log names, to send a subset of the logs. Defaults to False.
 
+@item addPatch
+(boolean). If True, include the patch content if a patch was present.
+Patches are usually used on a Try server.
+Defaults to True.
+
 @item relayhost
 (string). The host to which the outbound SMTP connection should be
 made. Defaults to 'localhost'
@@ -8767,6 +9022,11 @@ This context file can be a couple of kilobytes long, spanning a couple
 lines per patch, and would be a hassle to pass as a command-line
 argument.
 
+@item --property
+This parameter is used to set a property on the Change generated by sendchange.
+Properties are specified as a name:value pair, separated by a colon. You may
+specify many properties by passing this parameter multiple times.
+
 @item --comments
 This provides the change comments as a single argument. You may want
 to use @option{--logfile} instead.</diff>
      <filename>docs/buildbot.texinfo</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>84a0c8acccbf6074cd62904542b6467bb75741d4</id>
    </parent>
    <parent>
      <id>2b00a49451bad833f609b383747dcd1f9f947f3f</id>
    </parent>
  </parents>
  <author>
    <name>Matt Heitzenroder</name>
    <email>mheitzenroder@gmail.com</email>
  </author>
  <url>http://github.com/djmitche/buildbot/commit/1688df9a0576dbcf63be2bb9c81d8898c393d830</url>
  <id>1688df9a0576dbcf63be2bb9c81d8898c393d830</id>
  <committed-date>2009-11-04T18:22:38-08:00</committed-date>
  <authored-date>2009-11-04T18:22:38-08:00</authored-date>
  <message>simplified the code so it doesn't have to clone locally</message>
  <tree>696d6e1ac6d21924ce0c10baa9d38b21e8b9b99d</tree>
  <committer>
    <name>Matt Heitzenroder</name>
    <email>mheitzenroder@gmail.com</email>
  </committer>
</commit>
