Mon Apr 14 14:53:43 EDT 2008  dustin@v.igoro.us
  * #124:docs.patch
  Add properties to the documentation, along with a nontrivial reorganization
  of the scheduler section of the documentation.
Mon Apr 14 12:06:01 EDT 2008  dustin@v.igoro.us
  * wrap-long-doc-line-2.patch
  Wrap another long line
Mon Apr 14 12:03:44 EDT 2008  dustin@v.igoro.us
  * wrap-long-doc-line.patch
  Wrap an overly long line in the documentation.
Sun Apr 13 16:26:53 EDT 2008  dustin@v.igoro.us
  * #124:display-properties-web-status.patch
  Display build properties in the build status page.
Sun Apr 13 15:55:26 EDT 2008  dustin@v.igoro.us
  * #124:remove-custom-props.patch
  Remove custom properties, which are now largely redundant, and on which the
  properties interface was modeled.
Sun Apr 13 14:42:31 EDT 2008  dustin@v.igoro.us
  * #124:schedulers-provide-properties.patch
  Make the scheduler classes actually provide properties to the buildsets
  they submit.  Triggerable schedulers also do a nice job of configurably
  propagating properties from the triggering build.
Sat Apr 12 20:32:42 EDT 2008  dustin@v.igoro.us
  * #124:global-properties.patch
  Support global properties, defined with c['properties'] = {} in master.cfg
Sat Apr 12 19:46:58 EDT 2008  dustin@v.igoro.us
  * #124:scheduler-properties.patch
  Arrange for properties to come down from schedulers, via BuildStep and
  BuildRequest objects.  custom_props are still present, in parallel, but
  will go eventually. Triggered builds no longer propagate custom props.
Sat Apr 12 18:11:03 EDT 2008  dustin@v.igoro.us
  * #124:getProperty-returns-property-only.patch
  Change Property.getProperty to just return the property value, since
  having getProperty return different things on different objects is
  confusing.
Sat Apr 12 16:58:59 EDT 2008  dustin@v.igoro.us
  * #124:properties-class.patch
  Add and use a Properties class, refactor the way properties are rendered,
  and update unit tests accordingly.
Sat Apr 12 01:50:01 EDT 2008  dustin@v.igoro.us
  * #124:remove-customBuildProperties-unused.patch
  Remove unused and undocumented customBuildProperties config.
  This did not allow users to specify global properties; rather, it
  specified descriptions for properties that were not used anywhere in
  the codebase.
diff -rN -u old-124/buildbot/buildset.py new-124/buildbot/buildset.py
--- old-124/buildbot/buildset.py	2008-04-15 11:12:25.221116448 -0400
+++ new-124/buildbot/buildset.py	2008-04-15 11:12:25.289108515 -0400
@@ -1,6 +1,6 @@
-
 from buildbot.process import base
 from buildbot.status import builder
+from buildbot.process.properties import Properties
 
 
 class BuildSet:
@@ -11,7 +11,7 @@
     (source.changes=list)."""
 
     def __init__(self, builderNames, source, reason=None, bsid=None,
-                 scheduler=None, custom_props=None):
+                 properties=None):
         """
         @param source: a L{buildbot.sourcestamp.SourceStamp}
         """
@@ -19,13 +19,12 @@
         self.source = source
         self.reason = reason
 
-        if not custom_props: custom_props = {}
-        self.custom_props = custom_props
+        self.properties = Properties()
+        if properties: self.properties.updateFromProperties(properties)
 
         self.stillHopeful = True
         self.status = bss = builder.BuildSetStatus(source, reason,
                                                    builderNames, bsid)
-	self.scheduler = scheduler
 
     def waitUntilSuccess(self):
         return self.status.waitUntilSuccess()
@@ -41,8 +40,7 @@
         # create the requests
         for b in builders:
             req = base.BuildRequest(self.reason, self.source, b.name, 
-                                    scheduler=self.scheduler,
-                                    custom_props=self.custom_props)
+                                    properties=self.properties)
             reqs.append((b, req))
             self.requests.append(req)
             d = req.waitUntilFinished()
diff -rN -u old-124/buildbot/buildslave.py new-124/buildbot/buildslave.py
--- old-124/buildbot/buildslave.py	2008-04-15 11:12:25.221116448 -0400
+++ new-124/buildbot/buildslave.py	2008-04-15 11:12:25.289108515 -0400
@@ -11,6 +11,7 @@
 from buildbot.status.builder import SlaveStatus
 from buildbot.status.mail import MailNotifier
 from buildbot.interfaces import IBuildSlave
+from buildbot.process.properties import Properties
 
 class BuildSlave(NewCredPerspective, service.MultiService):
     """This is the master-side representative for a remote buildbot slave.
@@ -26,7 +27,8 @@
     implements(IBuildSlave)
 
     def __init__(self, name, password, max_builds=None,
-                 notify_on_missing=[], missing_timeout=3600):
+                 notify_on_missing=[], missing_timeout=3600,
+                 properties={}):
         """
         @param name: botname this machine will supply when it connects
         @param password: password this machine will supply when
@@ -34,6 +36,9 @@
         @param max_builds: maximum number of simultaneous builds that will
                            be run concurrently on this buildslave (the
                            default is None for no limit)
+        @param properties: properties that will be applied to builds run on 
+                           this slave
+        @type properties: dictionary
         """
         service.MultiService.__init__(self)
         self.slavename = name
@@ -44,6 +49,11 @@
         self.slave_commands = None
         self.slavebuilders = []
         self.max_builds = max_builds
+
+        self.properties = Properties()
+        self.properties.update(properties, "BuildSlave")
+        self.properties.setProperty("slavename", name, "BuildSlave")
+
         self.lastMessageReceived = 0
         if isinstance(notify_on_missing, str):
             notify_on_missing = [notify_on_missing]
diff -rN -u old-124/buildbot/clients/debug.py new-124/buildbot/clients/debug.py
--- old-124/buildbot/clients/debug.py	2008-04-15 11:12:25.181121114 -0400
+++ new-124/buildbot/clients/debug.py	2008-04-15 11:12:25.293108049 -0400
@@ -105,9 +105,9 @@
             if revision == '':
                 revision = None
         reason = "debugclient 'Request Build' button pushed"
-        custom_props = {}
+        properties = {}
         d = self.remote.callRemote("requestBuild",
-                                   name, reason, branch, revision, custom_props)
+                                   name, reason, branch, revision, properties)
         d.addErrback(self.err)
 
     def do_ping(self, widget):
diff -rN -u old-124/buildbot/interfaces.py new-124/buildbot/interfaces.py
--- old-124/buildbot/interfaces.py	2008-04-15 11:12:25.197119247 -0400
+++ new-124/buildbot/interfaces.py	2008-04-15 11:12:25.297107582 -0400
@@ -38,7 +38,12 @@
 class IScheduler(Interface):
     """I watch for Changes in the source tree and decide when to trigger
     Builds. I create BuildSet objects and submit them to the BuildMaster. I
-    am a service, and the BuildMaster is always my parent."""
+    am a service, and the BuildMaster is always my parent.
+    
+    @ivar properties: properties to be applied to all builds started by this
+    scheduler
+    @type properties: L<buildbot.process.properties.Properties>
+    """
 
     def addChange(change):
         """A Change has just been dispatched by one of the ChangeSources.
diff -rN -u old-124/buildbot/master.py new-124/buildbot/master.py
--- old-124/buildbot/master.py	2008-04-15 11:12:25.253112715 -0400
+++ new-124/buildbot/master.py	2008-04-15 11:12:25.297107582 -0400
@@ -27,6 +27,7 @@
 from buildbot.sourcestamp import SourceStamp
 from buildbot.buildslave import BuildSlave
 from buildbot import interfaces
+from buildbot.process.properties import Properties
 
 ########################################
 
@@ -227,19 +228,13 @@
     def detached(self, mind):
         pass
 
-    def perspective_requestBuild(self, buildername, reason, branch, revision, custom_props):
-        assert isinstance(custom_props, dict), \
-               "custom_props must be a dict (not %r)" % (custom_props,)
-
-        # Provide default values for any custom build properties the
-        # client did not send.
-        for propertyDict in (self.master.customBuildProperties or []):
-            custom_props.setdefault(propertyDict['propertyName'], "")
-
+    def perspective_requestBuild(self, buildername, reason, branch, revision, properties={}):
         c = interfaces.IControl(self.master)
         bc = c.getBuilder(buildername)
         ss = SourceStamp(branch, revision)
-        br = BuildRequest(reason, ss, builderName=buildername, custom_props=custom_props)
+        properties = Properties()
+        properties.update(properties, "remote requestBuild")
+        br = BuildRequest(reason, ss, builderName=buildername, properties=properties)
         bc.requestBuild(br)
 
     def perspective_pingBuilder(self, buildername):
@@ -347,7 +342,7 @@
     projectURL = None
     buildbotURL = None
     change_svc = None
-    customBuildProperties = None
+    properties = Properties()
 
     def __init__(self, basedir, configFileName="master.cfg"):
         service.MultiService.__init__(self)
@@ -503,7 +498,7 @@
                       "schedulers", "builders",
                       "slavePortnum", "debugPassword", "manhole",
                       "status", "projectName", "projectURL", "buildbotURL",
-                      "customBuildProperties"
+                      "properties"
                       )
         for k in config.keys():
             if k not in known_keys:
@@ -531,7 +526,7 @@
             projectName = config.get('projectName')
             projectURL = config.get('projectURL')
             buildbotURL = config.get('buildbotURL')
-            customBuildProperties = config.get('customBuildProperties')
+            properties = config.get('properties', {})
 
         except KeyError, e:
             log.msg("config dictionary is missing a required parameter")
@@ -663,6 +658,9 @@
                     else:
                         locks[l.name] = l
 
+        if not isinstance(properties, dict):
+            raise ValueError("c['properties'] must be a dictionary")
+
         # slavePortnum supposed to be a strports specification
         if type(slavePortnum) is int:
             slavePortnum = "tcp:%d" % slavePortnum
@@ -677,7 +675,9 @@
         self.projectName = projectName
         self.projectURL = projectURL
         self.buildbotURL = buildbotURL
-        self.customBuildProperties = customBuildProperties
+        
+        self.properties = Properties()
+        self.properties.update(properties, self.configFileName)
 
         # self.slaves: Disconnect any that were attached and removed from the
         # list. Update self.checker with the new list of passwords, including
diff -rN -u old-124/buildbot/process/base.py new-124/buildbot/process/base.py
--- old-124/buildbot/process/base.py	2008-04-15 11:12:25.249113181 -0400
+++ new-124/buildbot/process/base.py	2008-04-15 11:12:25.297107582 -0400
@@ -11,6 +11,7 @@
 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
 from buildbot.status.builder import Results, BuildRequestStatus
 from buildbot.status.progress import BuildProgress
+from buildbot.process.properties import Properties
 
 class BuildRequest:
     """I represent a request to a specific Builder to run a single build.
@@ -39,8 +40,8 @@
                   provide this, but for forced builds the user requesting the
                   build will provide a string.
 
-    @type custom_props: dictionary.
-    @ivar custom_props: custom user properties.
+    @type properties: Properties object
+    @ivar properties: properties that should be applied to this build
 
     @ivar status: the IBuildStatus object which tracks our status
 
@@ -52,22 +53,19 @@
     source = None
     builder = None
     startCount = 0 # how many times we have tried to start this build
-    custom_props = {}
 
     implements(interfaces.IBuildRequestControl)
 
-    def __init__(self, reason, source, builderName=None, scheduler=None, custom_props=None):
+    def __init__(self, reason, source, builderName=None, properties=None):
         # TODO: remove the =None on builderName, it is there so I don't have
         # to change a lot of tests that create BuildRequest objects
         assert interfaces.ISourceStamp(source, None)
         self.reason = reason
         self.source = source
-        self.scheduler = scheduler
 
-        if not custom_props: custom_props = {}
-        self.custom_props = custom_props
-        assert isinstance(self.custom_props, dict), \
-               "custom_props must be a dict (not %r)" % (self.custom_props,)
+        self.properties = Properties()
+        if properties:
+            self.properties.updateFromProperties(properties)
 
         self.start_watchers = []
         self.finish_watchers = []
@@ -95,9 +93,6 @@
         self.finish_watchers.append(d)
         return d
 
-    def customProps(self):
-     return self.custom_props
-
     # these are called by the Builder
 
     def requestSubmitted(self, builder):
@@ -182,12 +177,6 @@
         # build a source stamp
         self.source = requests[0].mergeWith(requests[1:])
         self.reason = requests[0].mergeReasons(requests[1:])
-        self.scheduler = requests[0].scheduler
-
-        # Set custom properties.
-        self.custom_properties = requests[0].customProps()
-
-        #self.abandoned = False
 
         self.progress = None
         self.currentStep = None
@@ -207,17 +196,17 @@
     def getSourceStamp(self):
         return self.source
 
-    def setProperty(self, propname, value):
+    def setProperty(self, propname, value, source):
         """Set a property on this build. This may only be called after the
         build has started, so that it has a BuildStatus object where the
         properties can live."""
-        self.build_status.setProperty(propname, value)
+        self.build_status.setProperty(propname, value, source)
 
-    def getCustomProperties(self):
-        return self.custom_properties
+    def getProperties(self):
+        return self.build_status.getProperties()
 
     def getProperty(self, propname):
-        return self.build_status.properties[propname]
+        return self.build_status.getProperty(propname)
 
     def allChanges(self):
         return self.source.changes
@@ -275,24 +264,35 @@
     def getSlaveName(self):
         return self.slavebuilder.slave.slavename
 
-    def setupStatus(self, build_status):
-        self.build_status = build_status
-        self.setProperty("buildername", self.builder.name)
-        self.setProperty("buildnumber", self.build_status.number)
-        self.setProperty("branch", self.source.branch)
-        self.setProperty("revision", self.source.revision)
-        if self.scheduler is None:
-            self.setProperty("scheduler", "none")
-        else:
-            self.setProperty("scheduler", self.scheduler.name)
-        for key, userProp in self.custom_properties.items():
-            self.setProperty(key, userProp)
+    def setupProperties(self):
+        props = self.getProperties()
+
+        # start with global properties from the configuration
+        buildmaster = self.builder.botmaster.parent
+        props.updateFromProperties(buildmaster.properties)
+
+        # get any properties from requests (this is the path through
+        # which schedulers will send us properties)
+        for rq in self.requests:
+            props.updateFromProperties(rq.properties)
+
+        # now set some properties of our own, corresponding to the
+        # build itself
+        props.setProperty("buildername", self.builder.name, "Build")
+        props.setProperty("buildnumber", self.build_status.number, "Build")
+        props.setProperty("branch", self.source.branch, "Build")
+        props.setProperty("revision", self.source.revision, "Build")
 
     def setupSlaveBuilder(self, slavebuilder):
         self.slavebuilder = slavebuilder
+
+        # navigate our way back to the L{buildbot.buildslave.BuildSlave}
+        # object that came from the config, and get its properties
+        buildslave_properties = slavebuilder.slave.properties
+        self.getProperties().updateFromProperties(buildslave_properties)
+
         self.slavename = slavebuilder.slave.slavename
         self.build_status.setSlavename(self.slavename)
-        self.setProperty("slavename", self.slavename)
 
     def startBuild(self, build_status, expectations, slavebuilder):
         """This method sets up the build, then starts it by invoking the
@@ -305,8 +305,9 @@
         # the Deferred returned by this method.
 
         log.msg("%s.startBuild" % self)
-        self.setupStatus(build_status)
+        self.build_status = build_status
         # now that we have a build_status, we can set properties
+        self.setupProperties()
         self.setupSlaveBuilder(slavebuilder)
 
         # convert all locks into their real forms
diff -rN -u old-124/buildbot/process/builder.py new-124/buildbot/process/builder.py
--- old-124/buildbot/process/builder.py	2008-04-15 11:12:25.217116914 -0400
+++ new-124/buildbot/process/builder.py	2008-04-15 11:12:25.297107582 -0400
@@ -253,6 +253,8 @@
     @type building: list of L{buildbot.process.base.Build}
     @ivar building: Builds that are actively running
 
+    @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
+    @ivar slaves: the slaves currently available for building
     """
 
     expectations = None # this is created the first time we get a good build
@@ -445,7 +447,7 @@
         """This is invoked by the BuildSlave when the self.slavename bot
         registers their builder.
 
-        @type  slave: L{buildbot.master.BuildSlave}
+        @type  slave: L{buildbot.buildslave.BuildSlave}
         @param slave: the BuildSlave that represents the buildslave as a whole
         @type  remote: L{twisted.spread.pb.RemoteReference}
         @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
diff -rN -u old-124/buildbot/process/buildstep.py new-124/buildbot/process/buildstep.py
--- old-124/buildbot/process/buildstep.py	2008-04-15 11:12:25.245113648 -0400
+++ new-124/buildbot/process/buildstep.py	2008-04-15 11:12:25.297107582 -0400
@@ -632,8 +632,8 @@
     def getProperty(self, propname):
         return self.build.getProperty(propname)
 
-    def setProperty(self, propname, value):
-        self.build.setProperty(propname, value)
+    def setProperty(self, propname, value, source):
+        self.build.setProperty(propname, value, source)
 
     def startStep(self, remote):
         """Begin the step. This returns a Deferred that will fire when the
@@ -1092,46 +1092,5 @@
         self.step_status.setText(self.getText(cmd, results))
         self.step_status.setText2(self.maybeGetText2(cmd, results))
 
-class _BuildPropertyMapping:
-    def __init__(self, build):
-        self.build = build
-    def __getitem__(self, name):
-        p = self.build.getProperty(name)
-        if p is None:
-            p = ""
-        return p
-
-class WithProperties(util.ComparableMixin):
-    """This is a marker class, used in ShellCommand's command= argument to
-    indicate that we want to interpolate a build property.
-    """
-
-    compare_attrs = ('fmtstring', 'args')
-
-    def __init__(self, fmtstring, *args):
-        self.fmtstring = fmtstring
-        self.args = args
-
-    def render(self, build):
-        pmap = _BuildPropertyMapping(build)
-        if self.args:
-            strings = []
-            for name in self.args:
-                strings.append(pmap[name])
-            s = self.fmtstring % tuple(strings)
-        else:
-            s = self.fmtstring % pmap
-        return s
-
-def render_properties(s, build):
-    """Return a string based on s and build that is suitable for use
-    in a running BuildStep.  If s is a string, return s.  If s is a
-    WithProperties object, return the result of s.render(build).
-    Otherwise, return str(s).
-    """
-    if isinstance(s, (str, unicode)):
-        return s
-    elif isinstance(s, WithProperties):
-        return s.render(build)
-    else:
-        return str(s)
+# (WithProeprties used to be available in this module)
+from buildbot.process.properties import WithProperties
diff -rN -u old-124/buildbot/process/properties.py new-124/buildbot/process/properties.py
--- old-124/buildbot/process/properties.py	1969-12-31 19:00:00.000000000 -0500
+++ new-124/buildbot/process/properties.py	2008-04-15 11:12:25.421093118 -0400
@@ -0,0 +1,105 @@
+from zope.interface import implements
+from buildbot import util
+from twisted.python import log
+from twisted.python.failure import Failure
+
+class Properties(util.ComparableMixin):
+    """
+    I represent a set of properties that can be interpolated into various
+    strings in buildsteps.
+
+    @ivar properties: dictionary mapping property values to tuples 
+        (value, source), where source is a string identifing the source
+        of the property.
+
+    Objects of this class can be read like a dictionary -- in this case,
+    only the property value is returned.
+
+    As a special case, a property value of None is returned as an empty 
+    string when used as a mapping.
+    """
+
+    compare_attrs = ('properties')
+
+    def __init__(self, **kwargs):
+        """
+        @param kwargs: initial property values (for testing)
+        """
+        self.properties = {}
+        if kwargs: self.update(kwargs, "TEST")
+
+    def __getitem__(self, name):
+        """Just get the value for this property, special-casing None -> ''"""
+        rv = self.properties[name][0]
+        if rv is None: rv = ''
+        return rv
+
+    def has_key(self, name):
+        return self.properties.has_key(name)
+
+    def getProperty(self, name, default=None):
+        """Get the value for the given property, with no None -> '' special case"""
+        return self.properties.get(name, (default,))[0]
+
+    def getPropertySource(self, name):
+        return self.properties[name][1]
+
+    def asList(self):
+        """Return the properties as a sorted list of (name, value, source)"""
+        l = [ (k, v[0], v[1]) for k,v in self.properties.items() ]
+        l.sort()
+        return l
+
+    def __repr__(self):
+        return repr(dict([ (k,v[0]) for k,v in self.properties.iteritems() ]))
+
+    def setProperty(self, name, value, source):
+        self.properties[name] = (value, source)
+
+    def update(self, dict, source):
+        """Update this object from a dictionary, with an explicit source specified."""
+        for k, v in dict.items():
+            self.properties[k] = (v, source)
+
+    def updateFromProperties(self, other):
+        """Update this object based on another object; the other object's """
+        self.properties.update(other.properties)
+
+    def render(self, value):
+        """
+        Return a variant of value that has any WithProperties objects
+        substituted.  This recurses into Python's compound data types.
+        """
+        if isinstance(value, (str, unicode)):
+            return value
+        elif isinstance(value, WithProperties):
+            return value.render(self)
+        elif isinstance(value, list):
+            return [ self.render(e) for e in value ]
+        elif isinstance(value, tuple):
+            return tuple([ self.render(e) for e in value ])
+        elif isinstance(value, dict):
+            return dict([ (self.render(k), self.render(v)) for k,v in value.iteritems() ])
+        else:
+            return value
+
+class WithProperties(util.ComparableMixin):
+    """This is a marker class, used in ShellCommand's command= argument to
+    indicate that we want to interpolate a build property.
+    """
+
+    compare_attrs = ('fmtstring', 'args')
+
+    def __init__(self, fmtstring, *args):
+        self.fmtstring = fmtstring
+        self.args = args
+
+    def render(self, properties):
+        if self.args:
+            strings = []
+            for name in self.args:
+                strings.append(properties[name])
+            s = self.fmtstring % tuple(strings)
+        else:
+            s = self.fmtstring % properties
+        return s
diff -rN -u old-124/buildbot/scheduler.py new-124/buildbot/scheduler.py
--- old-124/buildbot/scheduler.py	2008-04-15 11:12:25.213117381 -0400
+++ new-124/buildbot/scheduler.py	2008-04-15 11:12:25.301107116 -0400
@@ -14,20 +14,39 @@
 from buildbot.status import builder
 from buildbot.sourcestamp import SourceStamp
 from buildbot.changes.maildir import MaildirService
+from buildbot.process.properties import Properties
 
 
 class BaseScheduler(service.MultiService, util.ComparableMixin):
+    """
+    A Schduler creates BuildSets and submits them to the BuildMaster.
+
+    @ivar name: name of the scheduler
+
+    @ivar properties: additional properties specified in this 
+        scheduler's configuration
+    @type properties: Properties object
+    """
     implements(interfaces.IScheduler)
 
-    def __init__(self, name):
+    def __init__(self, name, properties={}):
+        """
+        @param name: name for this scheduler
+
+        @param properties: properties to be propagated from this scheduler
+        @type properties: dict
+        """
         service.MultiService.__init__(self)
         self.name = name
+        self.properties = Properties()
+        self.properties.update(properties, "Scheduler")
+        self.properties.setProperty("scheduler", name, "Scheduler")
 
     def __repr__(self):
         # TODO: why can't id() return a positive number? %d is ugly.
         return "<Scheduler '%s' at %d>" % (self.name, id(self))
 
-    def submit(self, bs):
+    def submitBuildSet(self, bs):
         self.parent.submitBuildSet(bs)
 
     def addChange(self, change):
@@ -36,8 +55,8 @@
 class BaseUpstreamScheduler(BaseScheduler):
     implements(interfaces.IUpstreamScheduler)
 
-    def __init__(self, name):
-        BaseScheduler.__init__(self, name)
+    def __init__(self, name, properties={}):
+        BaseScheduler.__init__(self, name, properties)
         self.successWatchers = []
 
     def subscribeToSuccessfulBuilds(self, watcher):
@@ -45,10 +64,10 @@
     def unsubscribeToSuccessfulBuilds(self, watcher):
         self.successWatchers.remove(watcher)
 
-    def submit(self, bs):
+    def submitBuildSet(self, bs):
         d = bs.waitUntilFinished()
         d.addCallback(self.buildSetFinished)
-        self.parent.submitBuildSet(bs)
+        BaseScheduler.submitBuildSet(self, bs)
 
     def buildSetFinished(self, bss):
         if not self.running:
@@ -69,10 +88,10 @@
 
     fileIsImportant = None
     compare_attrs = ('name', 'treeStableTimer', 'builderNames', 'branch',
-                     'fileIsImportant')
+                     'fileIsImportant', 'properties')
     
     def __init__(self, name, branch, treeStableTimer, builderNames,
-                 fileIsImportant=None):
+                 fileIsImportant=None, properties={}):
         """
         @param name: the name of this Scheduler
         @param branch: The branch name that the Scheduler should pay
@@ -94,9 +113,12 @@
                                 build is triggered by an important change.
                                 The default value of None means that all
                                 Changes are important.
+
+        @param properties: properties to apply to all builds started from this 
+                           scheduler
         """
 
-        BaseUpstreamScheduler.__init__(self, name)
+        BaseUpstreamScheduler.__init__(self, name, properties)
         self.treeStableTimer = treeStableTimer
         errmsg = ("The builderNames= argument to Scheduler must be a list "
                   "of Builder description names (i.e. the 'name' key of the "
@@ -171,8 +193,8 @@
         # create a BuildSet, submit it to the BuildMaster
         bs = buildset.BuildSet(self.builderNames,
                                SourceStamp(changes=changes),
-                               scheduler=self)
-        self.submit(bs)
+                               properties=self.properties)
+        self.submitBuildSet(bs)
 
     def stopService(self):
         self.stopTimer()
@@ -188,10 +210,10 @@
     fileIsImportant = None
 
     compare_attrs = ('name', 'branches', 'treeStableTimer', 'builderNames',
-                     'fileIsImportant')
+                     'fileIsImportant', 'properties')
 
     def __init__(self, name, branches, treeStableTimer, builderNames,
-                 fileIsImportant=None):
+                 fileIsImportant=None, properties={}):
         """
         @param name: the name of this Scheduler
         @param branches: The branch names that the Scheduler should pay
@@ -216,9 +238,12 @@
                                 build is triggered by an important change.
                                 The default value of None means that all
                                 Changes are important.
+
+        @param properties: properties to apply to all builds started from this 
+                           scheduler
         """
 
-        BaseUpstreamScheduler.__init__(self, name)
+        BaseUpstreamScheduler.__init__(self, name, properties)
         self.treeStableTimer = treeStableTimer
         for b in builderNames:
             assert isinstance(b, str)
@@ -272,19 +297,16 @@
             self.schedulers[branch] = s
         s.addChange(change)
 
-    def submitBuildSet(self, bs):
-        self.parent.submitBuildSet(bs)
-
 
 class Dependent(BaseUpstreamScheduler):
     """This scheduler runs some set of 'downstream' builds when the
     'upstream' scheduler has completed successfully."""
 
-    compare_attrs = ('name', 'upstream', 'builders')
+    compare_attrs = ('name', 'upstream', 'builders', 'properties')
 
-    def __init__(self, name, upstream, builderNames):
+    def __init__(self, name, upstream, builderNames, properties={}):
         assert interfaces.IUpstreamScheduler.providedBy(upstream)
-        BaseUpstreamScheduler.__init__(self, name)
+        BaseUpstreamScheduler.__init__(self, name, properties)
         self.upstream = upstream
         self.builderNames = builderNames
 
@@ -305,8 +327,9 @@
         return d
 
     def upstreamBuilt(self, ss):
-        bs = buildset.BuildSet(self.builderNames, ss, scheduler=self)
-        self.submit(bs)
+        bs = buildset.BuildSet(self.builderNames, ss,
+                    properties=self.properties)
+        self.submitBuildSet(bs)
 
 
 
@@ -319,11 +342,11 @@
     # TODO: consider having this watch another (changed-based) scheduler and
     # merely enforce a minimum time between builds.
 
-    compare_attrs = ('name', 'builderNames', 'periodicBuildTimer', 'branch')
+    compare_attrs = ('name', 'builderNames', 'periodicBuildTimer', 'branch', 'properties')
 
     def __init__(self, name, builderNames, periodicBuildTimer,
-                 branch=None):
-        BaseUpstreamScheduler.__init__(self, name)
+                 branch=None, properties={}):
+        BaseUpstreamScheduler.__init__(self, name, properties)
         self.builderNames = builderNames
         self.periodicBuildTimer = periodicBuildTimer
         self.branch = branch
@@ -344,8 +367,9 @@
     def doPeriodicBuild(self):
         bs = buildset.BuildSet(self.builderNames,
                                SourceStamp(branch=self.branch),
-                               self.reason, scheduler=self)
-        self.submit(bs)
+                               self.reason,
+                               properties=self.properties)
+        self.submitBuildSet(bs)
 
 
 
@@ -389,15 +413,15 @@
 
     compare_attrs = ('name', 'builderNames',
                      'minute', 'hour', 'dayOfMonth', 'month',
-                     'dayOfWeek', 'branch')
+                     'dayOfWeek', 'branch', 'properties')
 
     def __init__(self, name, builderNames, minute=0, hour='*',
                  dayOfMonth='*', month='*', dayOfWeek='*',
-                 branch=None):
+                 branch=None, properties={}):
         # Setting minute=0 really makes this an 'Hourly' scheduler. This
         # seemed like a better default than minute='*', which would result in
         # a build every 60 seconds.
-        BaseUpstreamScheduler.__init__(self, name)
+        BaseUpstreamScheduler.__init__(self, name, properties)
         self.builderNames = builderNames
         self.minute = minute
         self.hour = hour
@@ -502,20 +526,18 @@
         # And trigger a build
         bs = buildset.BuildSet(self.builderNames,
                                SourceStamp(branch=self.branch),
-                               self.reason, scheduler=self)
-        self.submit(bs)
+                               self.reason,
+                               properties=self.properties)
+        self.submitBuildSet(bs)
 
     def addChange(self, change):
         pass
 
 
 
-class TryBase(service.MultiService, util.ComparableMixin):
-    implements(interfaces.IScheduler)
-
-    def __init__(self, name, builderNames):
-        service.MultiService.__init__(self)
-        self.name = name
+class TryBase(BaseScheduler):
+    def __init__(self, name, builderNames, properties={}):
+        BaseScheduler.__init__(self, name, properties)
         self.builderNames = builderNames
 
     def listBuilderNames(self):
@@ -546,10 +568,10 @@
         self.error = True
 
 class Try_Jobdir(TryBase):
-    compare_attrs = ["name", "builderNames", "jobdir"]
+    compare_attrs = ( 'name', 'builderNames', 'jobdir', 'properties' )
 
-    def __init__(self, name, builderNames, jobdir):
-        TryBase.__init__(self, name, builderNames)
+    def __init__(self, name, builderNames, jobdir, properties={}):
+        TryBase.__init__(self, name, builderNames, properties)
         self.jobdir = jobdir
         self.watcher = MaildirService()
         self.watcher.setServiceParent(self)
@@ -624,15 +646,16 @@
                 return
 
         reason = "'try' job"
-        bs = buildset.BuildSet(builderNames, ss, reason=reason, bsid=bsid, scheduler=self)
-        self.parent.submitBuildSet(bs)
+        bs = buildset.BuildSet(builderNames, ss, reason=reason, 
+                    bsid=bsid, properties=self.properties)
+        self.submitBuildSet(bs)
 
 class Try_Userpass(TryBase):
-    compare_attrs = ["name", "builderNames", "port", "userpass"]
+    compare_attrs = ( 'name', 'builderNames', 'port', 'userpass', 'properties' )
     implements(portal.IRealm)
 
-    def __init__(self, name, builderNames, port, userpass):
-        TryBase.__init__(self, name, builderNames)
+    def __init__(self, name, builderNames, port, userpass, properties={}):
+        TryBase.__init__(self, name, builderNames, properties)
         if type(port) is int:
             port = "tcp:%d" % port
         self.port = port
@@ -657,16 +680,12 @@
         p = Try_Userpass_Perspective(self, avatarID)
         return (pb.IPerspective, p, lambda: None)
 
-    def submitBuildSet(self, bs):
-        return self.parent.submitBuildSet(bs)
-
 class Try_Userpass_Perspective(pbutil.NewCredPerspective):
     def __init__(self, parent, username):
         self.parent = parent
         self.username = username
 
-    def perspective_try(self, branch, revision, patch, builderNames,
-                        custom_props):
+    def perspective_try(self, branch, revision, patch, builderNames, properties={}):
         log.msg("user %s requesting build on builders %s" % (self.username,
                                                              builderNames))
         for b in builderNames:
@@ -678,11 +697,15 @@
         ss = SourceStamp(branch, revision, patch)
         reason = "'try' job from user %s" % self.username
 
+        # roll the specified props in with our inherited props
+        combined_props = Properties()
+        combined_props.updateFromProperties(self.parent.properties)
+        combined_props.update(properties, "try build")
+
         bs = buildset.BuildSet(builderNames, 
                                ss,
                                reason=reason, 
-                               scheduler=self,
-                               custom_props=custom_props)
+                               properties=combined_props)
 
         self.parent.submitBuildSet(bs)
 
@@ -696,8 +719,10 @@
     the builds that I fire have finished.
     """
 
-    def __init__(self, name, builderNames):
-        BaseUpstreamScheduler.__init__(self, name)
+    compare_attrs = ('name', 'builderNames', 'properties')
+
+    def __init__(self, name, builderNames, properties={}):
+        BaseUpstreamScheduler.__init__(self, name, properties)
         self.builderNames = builderNames
 
     def listBuilderNames(self):
@@ -706,11 +731,18 @@
     def getPendingBuildTimes(self):
         return []
 
-    def trigger(self, ss, custom_props={}):
+    def trigger(self, ss, set_props=None):
         """Trigger this scheduler. Returns a deferred that will fire when the
         buildset is finished.
         """
-        bs = buildset.BuildSet(self.builderNames, ss, scheduler=self, custom_props=custom_props)
+
+        # properties for this buildset are composed of our own properties,
+        # potentially overridden by anything from the triggering build
+        props = Properties()
+        props.updateFromProperties(self.properties)
+        if set_props: props.updateFromProperties(set_props)
+
+        bs = buildset.BuildSet(self.builderNames, ss, properties=props)
         d = bs.waitUntilFinished()
-        self.submit(bs)
+        self.submitBuildSet(bs)
         return d
diff -rN -u old-124/buildbot/scripts/runner.py new-124/buildbot/scripts/runner.py
--- old-124/buildbot/scripts/runner.py	2008-04-15 11:12:25.177121580 -0400
+++ new-124/buildbot/scripts/runner.py	2008-04-15 11:12:25.301107116 -0400
@@ -763,8 +763,8 @@
 
         ["builder", "b", None,
          "Run the trial build on this Builder. Can be used multiple times."],
-        ["customproperties", None, None,
-         "A set of custom properties made available in the build environment, format:prop=value,propb=valueb..."],
+        ["properties", None, None,
+         "A set of properties made available in the build environment, format:prop=value,propb=valueb..."],
         ]
 
     optFlags = [
@@ -774,21 +774,20 @@
     def __init__(self):
         super(TryOptions, self).__init__()
         self['builders'] = []
-        self['custom_props'] = {}
+        self['properties'] = {}
 
     def opt_builder(self, option):
         self['builders'].append(option)
 
-    def opt_customproperties(self, option):
-        # We need to split the value of this option into a dictionary of custom
-        # properties
-        custom_props = {}
+    def opt_properties(self, option):
+        # We need to split the value of this option into a dictionary of properties
+        properties = {}
         propertylist = option.split(",")
         for i in range(0,len(propertylist)):
             print propertylist[i]
             splitproperty = propertylist[i].split("=")
-            custom_props[splitproperty[0]] = splitproperty[1]
-        self['custom_props'] = custom_props
+            properties[splitproperty[0]] = splitproperty[1]
+        self['properties'] = properties
 
     def opt_patchlevel(self, option):
         self['patchlevel'] = int(option)
diff -rN -u old-124/buildbot/scripts/tryclient.py new-124/buildbot/scripts/tryclient.py
--- old-124/buildbot/scripts/tryclient.py	2008-04-15 11:12:25.177121580 -0400
+++ new-124/buildbot/scripts/tryclient.py	2008-04-15 11:12:25.301107116 -0400
@@ -448,7 +448,7 @@
                               ss.revision,
                               ss.patch,
                               self.builderNames,
-                              self.config.get('custom_props', {}))
+                              self.config.get('properties', {}))
         d.addCallback(self._deliverJob_pb2)
         return d
     def _deliverJob_pb2(self, status):
diff -rN -u old-124/buildbot/status/builder.py new-124/buildbot/status/builder.py
--- old-124/buildbot/status/builder.py	2008-04-15 11:12:25.249113181 -0400
+++ new-124/buildbot/status/builder.py	2008-04-15 11:12:25.305106649 -0400
@@ -5,6 +5,7 @@
 from twisted.persisted import styles
 from twisted.internet import reactor, defer
 from twisted.protocols import basic
+from buildbot.process.properties import Properties
 
 import os, shutil, sys, re, urllib, itertools
 from cPickle import load, dump
@@ -888,7 +889,7 @@
 
 class BuildStatus(styles.Versioned):
     implements(interfaces.IBuildStatus, interfaces.IStatusEvent)
-    persistenceVersion = 2
+    persistenceVersion = 3
 
     source = None
     reason = None
@@ -924,7 +925,7 @@
         self.finishedWatchers = []
         self.steps = []
         self.testResults = {}
-        self.properties = {}
+        self.properties = Properties()
 
     # IBuildStatus
 
@@ -937,6 +938,9 @@
     def getProperty(self, propname):
         return self.properties[propname]
 
+    def getProperties(self):
+        return self.properties
+
     def getNumber(self):
         return self.number
 
@@ -1074,8 +1078,8 @@
         self.steps.append(s)
         return s
 
-    def setProperty(self, propname, value):
-        self.properties[propname] = value
+    def setProperty(self, propname, value, source):
+        self.properties.setProperty(propname, value, source)
 
     def addTestResult(self, result):
         self.testResults[result.getName()] = result
@@ -1229,6 +1233,12 @@
     def upgradeToVersion2(self):
         self.properties = {}
 
+    def upgradeToVersion3(self):
+        # in version 3, self.properties became a Properties object
+        propdict = self.properties
+        self.properties = Properties()
+        self.properties.update(propdict)
+
     def upgradeLogfiles(self):
         # upgrade any LogFiles that need it. This must occur after we've been
         # attached to our Builder, and after we know about all LogFiles of
@@ -1789,8 +1799,6 @@
         return self.botmaster.parent.projectURL
     def getBuildbotURL(self):
         return self.botmaster.parent.buildbotURL
-    def getCustomBuildProperties(self):
-     return self.botmaster.parent.customBuildProperties
 
     def getURLForThing(self, thing):
         prefix = self.getBuildbotURL()
diff -rN -u old-124/buildbot/status/web/build.py new-124/buildbot/status/web/build.py
--- old-124/buildbot/status/web/build.py	2008-04-15 11:12:25.173122047 -0400
+++ new-124/buildbot/status/web/build.py	2008-04-15 11:12:25.309106182 -0400
@@ -121,6 +121,12 @@
                 data += " </li>\n"
             data += "</ol>\n"
 
+        data += "<h2>Build Properties:</h2>\n"
+        data += "<table><tr><th valign=\"left\">Name</th><th valign=\"left\">Value</th><th valign=\"left\">Source</th></tr>\n"
+        for name, value, source in b.getProperties().asList():
+            data += "<tr><td>%s</td><td>%s</td><td>%s</td></tr>\n" % (name, value, source)
+        data += "</table>"
+            
         data += "<h2>Blamelist:</h2>\n"
         if list(b.getResponsibleUsers()):
             data += " <ol>\n"
diff -rN -u old-124/buildbot/status/web/builder.py new-124/buildbot/status/web/builder.py
--- old-124/buildbot/status/web/builder.py	2008-04-15 11:12:25.173122047 -0400
+++ new-124/buildbot/status/web/builder.py	2008-04-15 11:12:25.309106182 -0400
@@ -152,7 +152,7 @@
 
         return data
 
-    def force(self, req, custom_props={}):
+    def force(self, req):
         """
 
         Custom properties can be passed from the web form.  To do
@@ -161,8 +161,6 @@
         by inspecting req.args), then pass them to this superclass
         force method.
         
-        @param custom_props: Custom properties to set on build
-        
         """
         name = req.args.get("username", ["<unknown>"])[0]
         reason = req.args.get("comments", ["<no reason specified>"])[0]
@@ -196,7 +194,7 @@
         # button, use their name instead of None, so they'll be informed of
         # the results.
         s = SourceStamp(branch=branch, revision=revision)
-        req = BuildRequest(r, s, builderName=self.builder_status.getName(), custom_props=custom_props)
+        req = BuildRequest(r, s, builderName=self.builder_status.getName())
         try:
             self.builder_control.requestBuildSoon(req)
         except interfaces.NoSlaveError:
diff -rN -u old-124/buildbot/steps/python.py new-124/buildbot/steps/python.py
--- old-124/buildbot/steps/python.py	2008-04-15 11:12:25.241114115 -0400
+++ new-124/buildbot/steps/python.py	2008-04-15 11:12:25.317105249 -0400
@@ -96,8 +96,8 @@
             if counts[m]:
                 self.descriptionDone.append("%s=%d" % (m, counts[m]))
                 self.addCompleteLog(m, "".join(summaries[m]))
-            self.setProperty("pyflakes-%s" % m, counts[m])
-        self.setProperty("pyflakes-total", sum(counts.values()))
+            self.setProperty("pyflakes-%s" % m, counts[m], "pyflakes")
+        self.setProperty("pyflakes-total", sum(counts.values()), "pyflakes")
 
 
     def evaluateCommand(self, cmd):
diff -rN -u old-124/buildbot/steps/shell.py new-124/buildbot/steps/shell.py
--- old-124/buildbot/steps/shell.py	2008-04-15 11:12:25.241114115 -0400
+++ new-124/buildbot/steps/shell.py	2008-04-15 11:12:25.317105249 -0400
@@ -2,12 +2,12 @@
 
 import re
 from twisted.python import log
-from buildbot.process.buildstep import LoggingBuildStep, RemoteShellCommand, \
-     render_properties
+from buildbot.process.buildstep import LoggingBuildStep, RemoteShellCommand
 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE
 
-# for existing configurations that import WithProperties from here
-from buildbot.process.buildstep import WithProperties
+# for existing configurations that import WithProperties from here.  We like
+# to move this class around just to keep our readers guessing.
+from buildbot.process.properties import WithProperties
 
 class ShellCommand(LoggingBuildStep):
     """I run a single shell command on the buildslave. I return FAILURE if
@@ -118,11 +118,12 @@
         if self.description is not None:
             return self.description
 
+        properties = self.build.getProperties()
         words = self.command
         if isinstance(words, (str, unicode)):
             words = words.split()
         # render() each word to handle WithProperties objects
-        words = [render_properties(word, self.build) for word in words]
+        words = properties.render(words)
         if len(words) < 1:
             return ["???"]
         if len(words) == 1:
@@ -131,45 +132,18 @@
             return ["'%s" % words[0], "%s'" % words[1]]
         return ["'%s" % words[0], "%s" % words[1], "...'"]
 
-    def _interpolateProperties(self, value):
-        """
-        Expand the L{WithProperties} objects in L{value}
-        """
-        if isinstance(value, (str, unicode, bool, int, float, type(None))):
-            return value
-
-        if isinstance(value, list):
-            return [self._interpolateProperties(val) for val in value]
-
-        if isinstance(value, tuple):
-            return tuple([self._interpolateProperties(val) for val in value])
-
-        if isinstance(value, dict):
-            new_dict = { }
-            for key, val in value.iteritems():
-                new_key = self._interpolateProperties(key)
-                new_dict[new_key] = self._interpolateProperties(val)
-            return new_dict
-
-        # To make sure we catch anything we forgot
-        assert isinstance(value, WithProperties), \
-               "%s (%s) is not a WithProperties" % (value, type(value))
-
-        return value.render(self.build)
-
-    def _interpolateWorkdir(self, workdir):
-        return render_properties(workdir, self.build)
-
     def setupEnvironment(self, cmd):
+        # XXX is this used? documented? replaced by properties?
         # merge in anything from Build.slaveEnvironment . Earlier steps
         # (perhaps ones which compile libraries or sub-projects that need to
         # be referenced by later steps) can add keys to
         # self.build.slaveEnvironment to affect later steps.
+        properties = self.build.getProperties()
         slaveEnv = self.build.slaveEnvironment
         if slaveEnv:
             if cmd.args['env'] is None:
                 cmd.args['env'] = {}
-            cmd.args['env'].update(self._interpolateProperties(slaveEnv))
+            cmd.args['env'].update(properties.render(slaveEnv))
             # note that each RemoteShellCommand gets its own copy of the
             # dictionary, so we shouldn't be affecting anyone but ourselves.
 
@@ -199,12 +173,11 @@
         # this block is specific to ShellCommands. subclasses that don't need
         # to set up an argv array, an environment, or extra logfiles= (like
         # the Source subclasses) can just skip straight to startCommand()
-        command = self._interpolateProperties(self.command)
-        assert isinstance(command, (list, tuple, str))
+        properties = self.build.getProperties()
+
         # create the actual RemoteShellCommand instance now
-        kwargs = self._interpolateProperties(self.remote_kwargs)
-        kwargs['workdir'] = self._interpolateWorkdir(kwargs['workdir'])
-        kwargs['command'] = command
+        kwargs = properties.render(self.remote_kwargs)
+        kwargs['command'] = properties.render(self.command)
         kwargs['logfiles'] = self.logfiles
         cmd = RemoteShellCommand(**kwargs)
         self.setupEnvironment(cmd)
@@ -224,7 +197,7 @@
         m = re.search(r'^(\d+)', out)
         if m:
             self.kib = int(m.group(1))
-            self.setProperty("tree-size-KiB", self.kib)
+            self.setProperty("tree-size-KiB", self.kib, "treesize")
 
     def evaluateCommand(self, cmd):
         if cmd.rc != 0:
@@ -298,7 +271,7 @@
             old_count = self.getProperty("warnings-count")
         except KeyError:
             old_count = 0
-        self.setProperty("warnings-count", old_count + self.warnCount)
+        self.setProperty("warnings-count", old_count + self.warnCount, "WarningCountingShellCommand")
 
 
     def evaluateCommand(self, cmd):
diff -rN -u old-124/buildbot/steps/source.py new-124/buildbot/steps/source.py
--- old-124/buildbot/steps/source.py	2008-04-15 11:12:25.237114581 -0400
+++ new-124/buildbot/steps/source.py	2008-04-15 11:12:25.317105249 -0400
@@ -187,7 +187,7 @@
         got_revision = None
         if cmd.updates.has_key("got_revision"):
             got_revision = str(cmd.updates["got_revision"][-1])
-        self.setProperty("got_revision", got_revision)
+        self.setProperty("got_revision", got_revision, "Source")
 
 
 
diff -rN -u old-124/buildbot/steps/transfer.py new-124/buildbot/steps/transfer.py
--- old-124/buildbot/steps/transfer.py	2008-04-15 11:12:25.237114581 -0400
+++ new-124/buildbot/steps/transfer.py	2008-04-15 11:12:25.377098250 -0400
@@ -4,8 +4,7 @@
 from twisted.internet import reactor
 from twisted.spread import pb
 from twisted.python import log
-from buildbot.process.buildstep import RemoteCommand, BuildStep, \
-     render_properties
+from buildbot.process.buildstep import RemoteCommand, BuildStep
 from buildbot.process.buildstep import SUCCESS, FAILURE
 from buildbot.interfaces import BuildSlaveTooOldError
 
@@ -111,12 +110,14 @@
 
     def start(self):
         version = self.slaveVersion("uploadFile")
+        properties = self.build.getProperties()
+
         if not version:
             m = "slave is too old, does not know about uploadFile"
             raise BuildSlaveTooOldError(m)
 
-        source = render_properties(self.slavesrc, self.build)
-        masterdest = render_properties(self.masterdest, self.build)
+        source = properties.render(self.slavesrc)
+        masterdest = properties.render(self.masterdest)
         # we rely upon the fact that the buildmaster runs chdir'ed into its
         # basedir to make sure that relative paths in masterdest are expanded
         # properly. TODO: maybe pass the master's basedir all the way down
@@ -237,6 +238,8 @@
         self.mode = mode
 
     def start(self):
+        properties = self.build.getProperties()
+
         version = self.slaveVersion("downloadFile")
         if not version:
             m = "slave is too old, does not know about downloadFile"
@@ -244,9 +247,8 @@
 
         # we are currently in the buildmaster's basedir, so any non-absolute
         # paths will be interpreted relative to that
-        source = os.path.expanduser(render_properties(self.mastersrc,
-                                                      self.build))
-        slavedest = render_properties(self.slavedest, self.build)
+        source = os.path.expanduser(properties.render(self.mastersrc))
+        slavedest = properties.render(self.slavedest)
         log.msg("FileDownload started, from master %r to slave %r" %
                 (source, slavedest))
 
diff -rN -u old-124/buildbot/steps/trigger.py new-124/buildbot/steps/trigger.py
--- old-124/buildbot/steps/trigger.py	2008-04-15 11:12:25.213117381 -0400
+++ new-124/buildbot/steps/trigger.py	2008-04-15 11:12:25.377098250 -0400
@@ -1,5 +1,5 @@
 from buildbot.process.buildstep import LoggingBuildStep, SUCCESS, FAILURE, EXCEPTION
-from buildbot.steps.shell import WithProperties
+from buildbot.process.properties import WithProperties, Properties
 from buildbot.scheduler import Triggerable
 from twisted.internet import defer
 
@@ -12,7 +12,7 @@
     flunkOnFailure = True
 
     def __init__(self, schedulerNames=[], updateSourceStamp=True,
-                 waitForFinish=False, **kwargs):
+                 waitForFinish=False, set_properties={}, **kwargs):
         """
         Trigger the given schedulers when this step is executed.
 
@@ -33,11 +33,19 @@
                               schedulers. If True, I will wait until all of
                               the triggered schedulers have finished their
                               builds.
+
+        @param set_properties: A dictionary of properties to set for any
+                               builds resulting from this trigger.  To copy
+                               existing properties, use WithProperties.  These
+                               properties will override properties set in the
+                               Triggered scheduler's constructor.
+
         """
         assert schedulerNames, "You must specify a scheduler to trigger"
         self.schedulerNames = schedulerNames
         self.updateSourceStamp = updateSourceStamp
         self.waitForFinish = waitForFinish
+        self.set_properties = set_properties
         self.running = False
         LoggingBuildStep.__init__(self, **kwargs)
         self.addFactoryArguments(schedulerNames=schedulerNames,
@@ -51,18 +59,20 @@
             self.step_status.setText(["interrupted"])
 
     def start(self):
-        custom_props = {}
+        properties = self.build.getProperties()
+
+        # make a new properties object from a dict rendered by the old 
+        # properties object
+        props_to_set = Properties()
+        props_to_set.update(properties.render(self.set_properties), "Trigger")
+
         self.running = True
         ss = self.build.getSourceStamp()
         if self.updateSourceStamp:
-            got = None
-            try:
-                got = self.build.getProperty('got_revision')
-            except KeyError:
-                pass
+            got = properties.getProperty('got_revision')
             if got:
                 ss = ss.getAbsoluteSourceStamp(got)
-        custom_props = self.build.getCustomProperties()
+
         # (is there an easier way to find the BuildMaster?)
         all_schedulers = self.build.builder.botmaster.parent.allSchedulers()
         all_schedulers = dict([(sch.name, sch) for sch in all_schedulers])
@@ -72,12 +82,11 @@
         # TODO: don't fire any schedulers if we discover an unknown one
         dl = []
         for scheduler in self.schedulerNames:
-            if isinstance(scheduler, WithProperties):
-                scheduler = scheduler.render(self.build)
+            scheduler = properties.render(scheduler)
             if all_schedulers.has_key(scheduler):
                 sch = all_schedulers[scheduler]
                 if isinstance(sch, Triggerable):
-                    dl.append(sch.trigger(ss, custom_props))
+                    dl.append(sch.trigger(ss, set_props=props_to_set))
                     triggered_schedulers.append(scheduler)
                 else:
                     unknown_schedulers.append(scheduler)
@@ -101,10 +110,8 @@
         else:
             d = defer.succeed([])
 
-        # TODO: review this shadowed 'rc' value: can the callback modify the
-        # one that was defined above?
         def cb(rclist):
-            rc = SUCCESS
+            rc = SUCCESS # (this rc is not the same variable as that above)
             for was_cb, buildsetstatus in rclist:
                 # TODO: make this algo more configurable
                 if not was_cb:
diff -rN -u old-124/buildbot/test/runutils.py new-124/buildbot/test/runutils.py
--- old-124/buildbot/test/runutils.py	2008-04-15 11:12:25.213117381 -0400
+++ new-124/buildbot/test/runutils.py	2008-04-15 11:12:25.381097784 -0400
@@ -13,6 +13,7 @@
 from buildbot.process.buildstep import BuildStep
 from buildbot.sourcestamp import SourceStamp
 from buildbot.status import builder
+from buildbot.process.properties import Properties
 
 
 
@@ -152,7 +153,7 @@
         if bs.getResults() != builder.SUCCESS:
             log.msg("failUnlessBuildSucceeded noticed that the build failed")
             self.logBuildResults(bs)
-        self.failUnless(bs.getResults() == builder.SUCCESS)
+        self.failUnlessEqual(bs.getResults(), builder.SUCCESS)
         return bs # useful for chaining
 
     def logBuildResults(self, bs):
@@ -275,6 +276,12 @@
     from buildbot.slave.registry import commandRegistry
     return commandRegistry[command]
 
+class FakeBuildMaster:
+    properties = Properties(masterprop="master")
+
+class FakeBotMaster:
+    parent = FakeBuildMaster()
+
 def makeBuildStep(basedir, step_class=BuildStep, **kwargs):
     bss = setupBuildStepStatus(basedir)
 
@@ -282,13 +289,15 @@
     setup = {'name': "builder1", "slavename": "bot1",
              'builddir': "builddir", 'factory': None}
     b0 = Builder(setup, bss.getBuild().getBuilder())
+    b0.botmaster = FakeBotMaster()
     br = BuildRequest("reason", ss)
     b = Build([br])
     b.setBuilder(b0)
     s = step_class(**kwargs)
     s.setBuild(b)
     s.setStepStatus(bss)
-    b.setupStatus(bss.getBuild())
+    b.build_status = bss.getBuild()
+    b.setupProperties()
     s.slaveVersion = fake_slaveVersion
     return s
 
@@ -480,7 +489,8 @@
         self.value = value
 
     def start(self):
-        _flags[self.flagname] = self.value
+        properties = self.build.getProperties()
+        _flags[self.flagname] = properties.render(self.value)
         self.finished(builder.SUCCESS)
 
 class TestFlagMixin:
diff -rN -u old-124/buildbot/test/test_properties.py new-124/buildbot/test/test_properties.py
--- old-124/buildbot/test/test_properties.py	2008-04-15 11:12:25.237114581 -0400
+++ new-124/buildbot/test/test_properties.py	2008-04-15 11:12:25.421093118 -0400
@@ -6,7 +6,8 @@
 
 from buildbot.sourcestamp import SourceStamp
 from buildbot.process import base
-from buildbot.steps.shell import ShellCommand, WithProperties
+from buildbot.process.properties import WithProperties, Properties
+from buildbot.steps.shell import ShellCommand
 from buildbot.status import builder
 from buildbot.slave.commands import rmdirRecursive
 from buildbot.test.runutils import RunMixin
@@ -14,11 +15,17 @@
 
 class FakeBuild:
     pass
+class FakeBuildMaster:
+    properties = Properties(masterprop="master")
+class FakeBotMaster:
+    parent = FakeBuildMaster()
 class FakeBuilder:
     statusbag = None
     name = "fakebuilder"
+    botmaster = FakeBotMaster()
 class FakeSlave:
     slavename = "bot12"
+    properties = Properties(slavename="bot12")
 class FakeSlaveBuilder:
     slave = FakeSlave()
     def getSlaveCommandVersion(self, command, oldversion=None):
@@ -26,101 +33,101 @@
 class FakeScheduler:
     name = "fakescheduler"
 
-class Interpolate(unittest.TestCase):
+class TestProperties(unittest.TestCase):
     def setUp(self):
-        self.builder = FakeBuilder()
-        self.builder_status = builder.BuilderStatus("fakebuilder")
-        self.builder_status.basedir = "test_properties"
-        self.builder_status.nextBuildNumber = 5
-        rmdirRecursive(self.builder_status.basedir)
-        os.mkdir(self.builder_status.basedir)
-        self.build_status = self.builder_status.newBuild()
-        req = base.BuildRequest("reason", SourceStamp(branch="branch2",
-                                                      revision=1234))
-        self.build = base.Build([req])
-        self.build.setBuilder(self.builder)
-        self.build.setupStatus(self.build_status)
-        self.build.setupSlaveBuilder(FakeSlaveBuilder())
+        self.props = Properties()
+
+    def testDictBehavior(self):
+        self.props.setProperty("do-tests", 1, "scheduler")
+        self.props.setProperty("do-install", 2, "scheduler")
+
+        self.assert_(self.props.has_key('do-tests'))
+        self.failUnlessEqual(self.props['do-tests'], 1)
+        self.failUnlessEqual(self.props['do-install'], 2)
+        self.assertRaises(KeyError, lambda : self.props['do-nothing'])
+        self.failUnlessEqual(self.props.getProperty('do-install'), 2)
+
+    def testEmpty(self):
+        # test the special case for Null
+        self.props.setProperty("x", None, "hi")
+        self.failUnlessEqual(self.props.getProperty('x'), None)
+        self.failUnlessEqual(self.props['x'], '')
+
+    def testUpdate(self):
+        self.props.setProperty("x", 24, "old")
+        newprops = { 'a' : 1, 'b' : 2 }
+        self.props.update(newprops, "new")
+
+        self.failUnlessEqual(self.props.getProperty('x'), 24)
+        self.failUnlessEqual(self.props.getPropertySource('x'), 'old')
+        self.failUnlessEqual(self.props.getProperty('a'), 1)
+        self.failUnlessEqual(self.props.getPropertySource('a'), 'new')
+
+    def testUpdateFromProperties(self):
+        self.props.setProperty("x", 24, "old")
+        newprops = Properties()
+        newprops.setProperty('a', 1, "new")
+        newprops.setProperty('b', 2, "new")
+        self.props.updateFromProperties(newprops)
+
+        self.failUnlessEqual(self.props.getProperty('x'), 24)
+        self.failUnlessEqual(self.props.getPropertySource('x'), 'old')
+        self.failUnlessEqual(self.props.getProperty('a'), 1)
+        self.failUnlessEqual(self.props.getPropertySource('a'), 'new')
+
+    # render() is pretty well tested by TestWithProperties
+
+class TestWithProperties(unittest.TestCase):
+    def setUp(self):
+        self.props = Properties()
 
-    def testWithProperties(self):
-        self.build.setProperty("revision", 47)
-        self.failUnlessEqual(self.build_status.getProperty("revision"), 47)
-        c = ShellCommand(workdir=dir,
-                         command=["tar", "czf",
-                                  WithProperties("build-%s.tar.gz",
-                                                 "revision"),
-                                  "source"])
-        c.setBuild(self.build)
-        cmd = c._interpolateProperties(c.command)
-        self.failUnlessEqual(cmd,
-                             ["tar", "czf", "build-47.tar.gz", "source"])
-        self.failUnlessEqual(self.build.getProperty("scheduler"), "none")
-
-    def testWorkdir(self):
-        self.build.setProperty("revision", 47)
-        self.failUnlessEqual(self.build_status.getProperty("revision"), 47)
-        c = ShellCommand(command=["tar", "czf", "foo.tar.gz", "source"])
-        c.setBuild(self.build)
-        workdir = WithProperties("workdir-%d", "revision")
-        workdir = c._interpolateWorkdir(workdir)
-        self.failUnlessEqual(workdir, "workdir-47")
-
-    def testWithPropertiesDict(self):
-        self.build.setProperty("other", "foo")
-        self.build.setProperty("missing", None)
-        c = ShellCommand(workdir=dir,
-                         command=["tar", "czf",
-                                  WithProperties("build-%(other)s.tar.gz"),
-                                  "source"])
-        c.setBuild(self.build)
-        cmd = c._interpolateProperties(c.command)
-        self.failUnlessEqual(cmd,
-                             ["tar", "czf", "build-foo.tar.gz", "source"])
-
-    def testWithPropertiesEmpty(self):
-        self.build.setProperty("empty", None)
-        c = ShellCommand(workdir=dir,
-                         command=["tar", "czf",
-                                  WithProperties("build-%(empty)s.tar.gz"),
-                                  "source"])
-        c.setBuild(self.build)
-        cmd = c._interpolateProperties(c.command)
-        self.failUnlessEqual(cmd,
-                             ["tar", "czf", "build-.tar.gz", "source"])
-
-    def testSourceStamp(self):
-        c = ShellCommand(workdir=dir,
-                         command=["touch",
-                                  WithProperties("%s-dir", "branch"),
-                                  WithProperties("%s-rev", "revision"),
-                                  ])
-        c.setBuild(self.build)
-        cmd = c._interpolateProperties(c.command)
-        self.failUnlessEqual(cmd,
-                             ["touch", "branch2-dir", "1234-rev"])
-
-    def testSlaveName(self):
-        c = ShellCommand(workdir=dir,
-                         command=["touch",
-                                  WithProperties("%s-slave", "slavename"),
-                                  ])
-        c.setBuild(self.build)
-        cmd = c._interpolateProperties(c.command)
-        self.failUnlessEqual(cmd,
-                             ["touch", "bot12-slave"])
-
-    def testBuildNumber(self):
-        c = ShellCommand(workdir=dir,
-                         command=["touch",
-                                  WithProperties("build-%d", "buildnumber"),
-                                  WithProperties("builder-%s", "buildername"),
-                                  ])
-        c.setBuild(self.build)
-        cmd = c._interpolateProperties(c.command)
-        self.failUnlessEqual(cmd,
-                             ["touch", "build-5", "builder-fakebuilder"])
+    def testBasic(self):
+        # test basic substitution with WithProperties
+        self.props.setProperty("revision", "47", "test")
+        command = WithProperties("build-%s.tar.gz", "revision")
+        self.failUnlessEqual(self.props.render(command),
+                             "build-47.tar.gz")
+
+    def testDict(self):
+        # test dict-style substitution with WithProperties
+        self.props.setProperty("other", "foo", "test")
+        command = WithProperties("build-%(other)s.tar.gz")
+        self.failUnlessEqual(self.props.render(command),
+                             "build-foo.tar.gz")
+
+    def testEmpty(self):
+        # None should render as ''
+        self.props.setProperty("empty", None, "test")
+        command = WithProperties("build-%(empty)s.tar.gz")
+        self.failUnlessEqual(self.props.render(command),
+                             "build-.tar.gz")
+
+    def testRecursiveList(self):
+        self.props.setProperty("x", 10, "test")
+        self.props.setProperty("y", 20, "test")
+        command = [ WithProperties("%(x)s %(y)s"), "and",
+                    WithProperties("%(y)s %(x)s") ]
+        self.failUnlessEqual(self.props.render(command),
+                             ["10 20", "and", "20 10"])
+
+    def testRecursiveTuple(self):
+        self.props.setProperty("x", 10, "test")
+        self.props.setProperty("y", 20, "test")
+        command = ( WithProperties("%(x)s %(y)s"), "and",
+                    WithProperties("%(y)s %(x)s") )
+        self.failUnlessEqual(self.props.render(command),
+                             ("10 20", "and", "20 10"))
+
+    def testRecursiveDict(self):
+        self.props.setProperty("x", 10, "test")
+        self.props.setProperty("y", 20, "test")
+        command = { WithProperties("%(x)s %(y)s") : 
+                    WithProperties("%(y)s %(x)s") }
+        self.failUnlessEqual(self.props.render(command),
+                             {"10 20" : "20 10"})
 
-class SchedulerTest(unittest.TestCase):
+class BuildProperties(unittest.TestCase):
+    """Test the properties that a build should have."""
     def setUp(self):
         self.builder = FakeBuilder()
         self.builder_status = builder.BuilderStatus("fakebuilder")
@@ -129,16 +136,23 @@
         rmdirRecursive(self.builder_status.basedir)
         os.mkdir(self.builder_status.basedir)
         self.build_status = self.builder_status.newBuild()
-        req = base.BuildRequest("reason", SourceStamp(branch="branch2",
-                                revision=1234), scheduler=FakeScheduler())
+        req = base.BuildRequest("reason", 
+                    SourceStamp(branch="branch2", revision="1234"),
+                    properties=Properties(scheduler="fakescheduler"))
         self.build = base.Build([req])
+        self.build.build_status = self.build_status
         self.build.setBuilder(self.builder)
-        self.build.setupStatus(self.build_status)
+        self.build.setupProperties()
         self.build.setupSlaveBuilder(FakeSlaveBuilder())
 
-    def testWithScheduler(self):
-        self.failUnlessEqual(self.build.getProperty("scheduler"),
-                             "fakescheduler")
+    def testProperties(self):
+        self.failUnlessEqual(self.build.getProperty("scheduler"), "fakescheduler")
+        self.failUnlessEqual(self.build.getProperty("branch"), "branch2")
+        self.failUnlessEqual(self.build.getProperty("revision"), "1234")
+        self.failUnlessEqual(self.build.getProperty("slavename"), "bot12")
+        self.failUnlessEqual(self.build.getProperty("buildnumber"), 5)
+        self.failUnlessEqual(self.build.getProperty("buildername"), "fakebuilder")
+        self.failUnlessEqual(self.build.getProperty("masterprop"), "master")
 
 run_config = """
 from buildbot.process import factory
@@ -147,9 +161,10 @@
 s = factory.s
 
 BuildmasterConfig = c = {}
-c['slaves'] = [BuildSlave('bot1', 'sekrit')]
+c['slaves'] = [BuildSlave('bot1', 'sekrit', properties={'slprop':'slprop'})]
 c['schedulers'] = []
 c['slavePortnum'] = 0
+c['properties'] = { 'global' : 'global' }
 
 # Note: when run against twisted-1.3.0, this locks up about 5% of the time. I
 # suspect that a command with no output that finishes quickly triggers a race
@@ -160,7 +175,8 @@
 f1 = factory.BuildFactory([s(ShellCommand,
                              flunkOnFailure=True,
                              command=['touch',
-                                      WithProperties('%s-slave', 'slavename'),
+                                      WithProperties('%s-%s-%s',
+                                        'slavename', 'global', 'slprop'),
                                       ],
                              workdir='.',
                              timeout=10,
@@ -180,7 +196,7 @@
         d.addCallback(lambda res: self.requestBuild("full1"))
         d.addCallback(self.failUnlessBuildSucceeded)
         def _check_touch(res):
-            f = os.path.join("slavebase-bot1", "bd1", "bot1-slave")
+            f = os.path.join("slavebase-bot1", "bd1", "bot1-global-slprop")
             self.failUnless(os.path.exists(f))
             return res
         d.addCallback(_check_touch)
diff -rN -u old-124/buildbot/test/test_run.py new-124/buildbot/test/test_run.py
--- old-124/buildbot/test/test_run.py	2008-04-15 11:12:25.185120647 -0400
+++ new-124/buildbot/test/test_run.py	2008-04-15 11:12:25.385097317 -0400
@@ -699,6 +699,108 @@
         self.failIfFlagSet('triggeree_finished')
         self.failIfFlagSet('triggerer_finished')
 
+class PropertyPropagation(RunMixin, TestFlagMixin, unittest.TestCase):
+    def setupTest(self, config, builders, checkFn):
+        self.clearFlags()
+        m = self.master
+        m.loadConfig(config)
+        m.readConfig = True
+        m.startService()
+
+        c = changes.Change("bob", ["Makefile", "foo/bar.c"], "changed stuff")
+        m.change_svc.addChange(c)
+
+        d = self.connectSlave(builders=builders)
+        d.addCallback(self.startTimer, 0.5, checkFn)
+        return d
+
+    def startTimer(self, res, time, next_fn):
+        d = defer.Deferred()
+        reactor.callLater(time, d.callback, None)
+        d.addCallback(next_fn)
+        return d
+
+    config_schprop = config_base + """
+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')),
+    ])
+c['builders'] = [{'name': 'flagcolor', 'slavename': 'bot1',
+                  'builddir': 'test', 'factory': factory},
+                ]
+"""
+
+    def testScheduler(self):
+        def _check(res):
+            self.failUnlessEqual(self.getFlag('testresult'),
+                'color=red sched=mysched')
+        return self.setupTest(self.config_schprop, ['flagcolor'], _check)
+
+    config_slaveprop = config_base + """
+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'])
+]
+c['slaves'] = [BuildSlave('bot1', 'sekrit', properties={'color':'orange'})]
+factory = factory.BuildFactory([
+    s(SetTestFlagStep, flagname='testresult', 
+      value=WithProperties('color=%(color)s slavename=%(slavename)s')),
+    ])
+c['builders'] = [{'name': 'flagcolor', 'slavename': 'bot1',
+                  'builddir': 'test', 'factory': factory},
+                ]
+"""
+    def testSlave(self):
+        def _check(res):
+            self.failUnlessEqual(self.getFlag('testresult'),
+                'color=orange slavename=bot1')
+        return self.setupTest(self.config_slaveprop, ['flagcolor'], _check)
+
+    config_trigger = config_base + """
+from buildbot.scheduler import Triggerable, Scheduler
+from buildbot.steps.trigger import Trigger
+from buildbot.steps.dummy import Dummy
+from buildbot.test.runutils import SetTestFlagStep
+from buildbot.process.properties import WithProperties
+c['schedulers'] = [
+    Scheduler('triggerer', None, 0.1, ['triggerer'], 
+        properties={'color':'mauve', 'pls_trigger':'triggeree'}),
+    Triggerable('triggeree', ['triggeree'], properties={'color':'invisible'})
+]
+triggerer = factory.BuildFactory([
+    s(SetTestFlagStep, flagname='testresult', value='wrongone'),
+    s(Trigger, flunkOnFailure=True, 
+        schedulerNames=[WithProperties('%(pls_trigger)s')],
+        set_properties={'color' : WithProperties('%(color)s')}),
+    s(SetTestFlagStep, flagname='testresult', value='triggered'),
+    ])
+triggeree = factory.BuildFactory([
+    s(SetTestFlagStep, flagname='testresult', 
+        value=WithProperties('sched=%(scheduler)s color=%(color)s')),
+    ])
+c['builders'] = [{'name': 'triggerer', 'slavename': 'bot1',
+                  'builddir': 'triggerer', 'factory': triggerer},
+                 {'name': 'triggeree', 'slavename': 'bot1',
+                  'builddir': 'triggeree', 'factory': triggeree}]
+"""
+    def testTrigger(self):
+        def _check(res):
+            self.failUnlessEqual(self.getFlag('testresult'),
+                'sched=triggeree color=mauve')
+        return self.setupTest(self.config_trigger, 
+                ['triggerer', 'triggeree'], _check)
+
+
 config_test_flag = config_base + """
 from buildbot.scheduler import Scheduler
 c['schedulers'] = [Scheduler('quick', None, 0.1, ['dummy'])]
diff -rN -u old-124/buildbot/test/test_steps.py new-124/buildbot/test/test_steps.py
--- old-124/buildbot/test/test_steps.py	2008-04-15 11:12:25.237114581 -0400
+++ new-124/buildbot/test/test_steps.py	2008-04-15 11:12:25.389096850 -0400
@@ -236,13 +236,13 @@
         s = makeBuildStep("test_steps.Steps.test_getProperty")
         bs = s.step_status.getBuild()
 
-        s.setProperty("prop1", "value1")
-        s.setProperty("prop2", "value2")
+        s.setProperty("prop1", "value1", "test")
+        s.setProperty("prop2", "value2", "test")
         self.failUnlessEqual(s.getProperty("prop1"), "value1")
         self.failUnlessEqual(bs.getProperty("prop1"), "value1")
         self.failUnlessEqual(s.getProperty("prop2"), "value2")
         self.failUnlessEqual(bs.getProperty("prop2"), "value2")
-        s.setProperty("prop1", "value1a")
+        s.setProperty("prop1", "value1a", "test")
         self.failUnlessEqual(s.getProperty("prop1"), "value1a")
         self.failUnlessEqual(bs.getProperty("prop1"), "value1a")
 
@@ -601,7 +601,7 @@
 line 23: warning: we are now on line 23
 ending line
 """
-        step.setProperty("warnings-count", 10)
+        step.setProperty("warnings-count", 10, "test")
         log = step.addLog("stdio")
         log.addStdout(output)
         log.finish()
diff -rN -u old-124/buildbot/test/test_vc.py new-124/buildbot/test/test_vc.py
--- old-124/buildbot/test/test_vc.py	2008-04-15 11:12:25.233115048 -0400
+++ new-124/buildbot/test/test_vc.py	2008-04-15 11:12:25.389096850 -0400
@@ -541,8 +541,8 @@
         self.shouldExist(self.workdir, "subdir", "subdir.c")
         if self.metadir:
             self.shouldExist(self.workdir, self.metadir)
-        self.failUnlessEqual(bs.getProperty("revision"), None)
-        self.failUnlessEqual(bs.getProperty("branch"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), '')
+        self.failUnlessEqual(bs.getProperty("branch"), '')
         self.checkGotRevisionIsLatest(bs)
 
         self.touch(self.workdir, "newfile")
@@ -565,8 +565,8 @@
         self.shouldExist(self.workdir, "subdir", "subdir.c")
         if self.metadir:
             self.shouldExist(self.workdir, self.metadir)
-        self.failUnlessEqual(bs.getProperty("revision"), self.helper.trunk[0])
-        self.failUnlessEqual(bs.getProperty("branch"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), self.helper.trunk[0] or '')
+        self.failUnlessEqual(bs.getProperty("branch"), '')
         self.checkGotRevision(bs, self.helper.trunk[0])
         # leave the tree at HEAD
         return self.doBuild()
@@ -585,7 +585,7 @@
                            "version=%d" % self.helper.version)
         if self.metadir:
             self.shouldExist(self.workdir, self.metadir)
-        self.failUnlessEqual(bs.getProperty("revision"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), '')
         self.checkGotRevisionIsLatest(bs)
 
         self.touch(self.workdir, "newfile")
@@ -609,7 +609,7 @@
         self.shouldContain(self.workdir, "version.c",
                            "version=%d" % self.helper.version)
         self.shouldExist(self.workdir, "newfile")
-        self.failUnlessEqual(bs.getProperty("revision"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), '')
         self.checkGotRevisionIsLatest(bs)
 
         # now "update" to an older revision
@@ -623,7 +623,7 @@
         self.shouldContain(self.workdir, "version.c",
                            "version=%d" % (self.helper.version-1))
         self.failUnlessEqual(bs.getProperty("revision"),
-                             self.helper.trunk[-2])
+                             self.helper.trunk[-2] or '')
         self.checkGotRevision(bs, self.helper.trunk[-2])
 
         # now update to the newer revision
@@ -637,7 +637,7 @@
         self.shouldContain(self.workdir, "version.c",
                            "version=%d" % self.helper.version)
         self.failUnlessEqual(bs.getProperty("revision"),
-                             self.helper.trunk[-1])
+                             self.helper.trunk[-1] or '')
         self.checkGotRevision(bs, self.helper.trunk[-1])
 
 
@@ -674,7 +674,7 @@
         self.shouldNotExist(self.workdir, "newfile")
         self.touch(self.workdir, "newfile")
         self.touch(self.vcdir, "newvcfile")
-        self.failUnlessEqual(bs.getProperty("revision"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), '')
         self.checkGotRevisionIsLatest(bs)
 
         d = self.doBuild() # copy rebuild clobbers new files
@@ -687,7 +687,7 @@
         self.shouldNotExist(self.workdir, "newfile")
         self.shouldExist(self.vcdir, "newvcfile")
         self.shouldExist(self.workdir, "newvcfile")
-        self.failUnlessEqual(bs.getProperty("revision"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), '')
         self.checkGotRevisionIsLatest(bs)
         self.touch(self.workdir, "newfile")
 
@@ -698,7 +698,7 @@
     def _do_vctest_export_1(self, bs):
         self.shouldNotExist(self.workdir, self.metadir)
         self.shouldNotExist(self.workdir, "newfile")
-        self.failUnlessEqual(bs.getProperty("revision"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), '')
         #self.checkGotRevisionIsLatest(bs)
         # VC 'export' is not required to have a got_revision
         self.touch(self.workdir, "newfile")
@@ -709,7 +709,7 @@
     def _do_vctest_export_2(self, bs):
         self.shouldNotExist(self.workdir, self.metadir)
         self.shouldNotExist(self.workdir, "newfile")
-        self.failUnlessEqual(bs.getProperty("revision"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), '')
         #self.checkGotRevisionIsLatest(bs)
         # VC 'export' is not required to have a got_revision
 
@@ -744,7 +744,7 @@
         data = open(subdir_c, "r").read()
         self.failUnlessIn("Hello patched subdir.\\n", data)
         self.failUnlessEqual(bs.getProperty("revision"),
-                             self.helper.trunk[-1])
+                             self.helper.trunk[-1] or '')
         self.checkGotRevision(bs, self.helper.trunk[-1])
 
         # make sure that a rebuild does not use the leftover patched workdir
@@ -758,7 +758,7 @@
                                 "subdir", "subdir.c")
         data = open(subdir_c, "r").read()
         self.failUnlessIn("Hello subdir.\\n", data)
-        self.failUnlessEqual(bs.getProperty("revision"), None)
+        self.failUnlessEqual(bs.getProperty("revision"), '')
         self.checkGotRevisionIsLatest(bs)
 
         # now make sure we can patch an older revision. We need at least two
@@ -783,7 +783,7 @@
         data = open(subdir_c, "r").read()
         self.failUnlessIn("Hello patched subdir.\\n", data)
         self.failUnlessEqual(bs.getProperty("revision"),
-                             self.helper.trunk[-2])
+                             self.helper.trunk[-2] or '')
         self.checkGotRevision(bs, self.helper.trunk[-2])
 
         # now check that we can patch a branch
@@ -802,8 +802,8 @@
         data = open(subdir_c, "r").read()
         self.failUnlessIn("Hello patched subdir.\\n", data)
         self.failUnlessEqual(bs.getProperty("revision"),
-                             self.helper.branch[-1])
-        self.failUnlessEqual(bs.getProperty("branch"), self.helper.branchname)
+                             self.helper.branch[-1] or '')
+        self.failUnlessEqual(bs.getProperty("branch"), self.helper.branchname or '')
         self.checkGotRevision(bs, self.helper.branch[-1])
 
 
diff -rN -u old-124/docs/Makefile new-124/docs/Makefile
--- old-124/docs/Makefile	2008-04-15 11:12:25.161123447 -0400
+++ new-124/docs/Makefile	2008-04-15 11:12:25.397095917 -0400
@@ -2,10 +2,12 @@
 buildbot.info: buildbot.texinfo
 	makeinfo --fill-column=70 $<
 
-buildbot.html: buildbot.texinfo images-png
+#images-png
+buildbot.html: buildbot.texinfo
 	makeinfo --no-split --html $<
 
-buildbot.ps: buildbot.texinfo images-eps
+#images-eps
+buildbot.ps: buildbot.texinfo
 	texi2dvi $<
 	dvips buildbot.dvi
 	rm buildbot.aux buildbot.cp buildbot.cps buildbot.fn buildbot.ky buildbot.log buildbot.pg buildbot.toc buildbot.tp buildbot.vr
diff -rN -u old-124/docs/buildbot.texinfo new-124/docs/buildbot.texinfo
--- old-124/docs/buildbot.texinfo	2008-04-15 11:12:25.169122514 -0400
+++ new-124/docs/buildbot.texinfo	2008-04-15 11:12:25.421093118 -0400
@@ -109,6 +109,7 @@
 * BuildRequest::                
 * Builder::                     
 * Users::                       
+* Build Properties::
 
 Version Control Systems
 
@@ -130,17 +131,23 @@
 * Loading the Config File::     
 * Testing the Config File::     
 * Defining the Project::        
-* Listing Change Sources and Schedulers::  
+* Change Sources and Schedulers::  
 * Setting the slaveport::       
 * Buildslave Specifiers::       
+* Defining Global Properties::           
 * Defining Builders::           
 * Defining Status Targets::     
 * Debug options::               
 
-Listing Change Sources and Schedulers
+Change Sources and Schedulers
 
-* Scheduler Types::             
-* Build Dependencies::          
+* Scheduler Scheduler::             
+* AnyBranchScheduler::          
+* Dependent Scheduler::          
+* Periodic Scheduler::          
+* Nightly Scheduler::          
+* Try Schedulers::          
+* Triggerable Scheduler::          
 
 Buildslave Specifiers
 
@@ -180,6 +187,7 @@
 Build Steps
 
 * Common Parameters::           
+* Using Build Properties::
 * Source Checkout::             
 * ShellCommand::                
 * Simple ShellCommand Subclasses::  
@@ -206,7 +214,6 @@
 * Compile::                     
 * Test::                        
 * TreeSize::                    
-* Build Properties::            
 
 Python BuildSteps
 
@@ -1231,6 +1238,7 @@
 * BuildRequest::                
 * Builder::                     
 * Users::                       
+* Build Properties::
 @end menu
 
 @node Version Control Systems, Schedulers, Concepts, Concepts
@@ -1756,7 +1764,7 @@
 OS-X-based buildslave.
 
 
-@node Users,  , Builder, Concepts
+@node Users, Build Properties, Builder, Concepts
 @section Users
 
 @cindex Users
@@ -1897,6 +1905,49 @@
 alternative way to deliver low-latency high-interruption messages to the
 developer (like ``hey, you broke the build'').
 
+@node Build Properties, , Users, Concepts
+@section Build Properties
+@cindex Properties
+
+Each build has a set of ``Build Properties'', which can be used by its
+BuildStep to modify their actions.  These properties, in the form of
+key-value pairs, provide a general framework for dynamically altering
+the behavior of a build based on its circumstances.
+
+Properties come from a number of places:
+@itemize
+@item global configuration --
+These properties apply to all builds.
+@item schedulers --
+A scheduler can specify properties available to all the builds it 
+starts.
+@item buildslaves --
+A buildslave can pass properties on to the builds it performs.
+@item builds --
+A build automatically sets a number of properties on itself.
+@item steps --
+Steps of a build can set properties that are available to subsequent
+steps.  In particular, source steps set a number of properties.
+@end itemize
+
+Properties are very flexible, and can be used to implement all manner
+of functionality.  Here are some examples:
+
+Most Source steps record the revision that they checked out in
+the @code{got_revision} property.  A later step could use this
+property to specify the name of a fully-built tarball, dropped in an
+easily-acessible directory for later testing.
+
+Some projects want to perform nightly builds as well as in response
+to committed changes.  Such a project would run two schedulers,
+both pointing to the same set of builders, but could provide an
+@code{is_nightly} property so that steps can distinguish the nightly
+builds, perhaps to run more resource-intensive tests.
+
+Some projects have different build processes on different systems.
+Rather than create a build factory for each slave, the steps can use
+buildslave properties to identify the unique aspects of each slave
+and adapt the build process dynamically.
 
 @node Configuration, Getting Source Code Changes, Concepts, Top
 @chapter Configuration
@@ -1925,9 +1976,10 @@
 * Loading the Config File::     
 * Testing the Config File::     
 * Defining the Project::        
-* Listing Change Sources and Schedulers::  
+* Change Sources and Schedulers::  
 * Setting the slaveport::       
 * Buildslave Specifiers::       
+* Defining Global Properties::           
 * Defining Builders::           
 * Defining Status Targets::     
 * Debug options::               
@@ -2049,7 +2101,8 @@
 
 @example
 % buildbot checkconfig master.cfg
-/usr/lib/python2.4/site-packages/buildbot/master.py:559: DeprecationWarning: c['sources'] is deprecated as of 0.7.6 and will be removed by 0.8.0 . Please use c['change_source'] instead.
+/usr/lib/python2.4/site-packages/buildbot/master.py:559: DeprecationWarning: c['sources'] is
+deprecated as of 0.7.6 and will be removed by 0.8.0 . Please use c['change_source'] instead.
   warnings.warn(m, DeprecationWarning)
 Config file is good!
 @end example
@@ -2071,7 +2124,7 @@
 @end example
 
 
-@node Defining the Project, Listing Change Sources and Schedulers, Testing the Config File, Configuration
+@node Defining the Project, Change Sources and Schedulers, Testing the Config File, Configuration
 @section Defining the Project
 
 There are a couple of basic settings that you use to tell the buildbot
@@ -2110,8 +2163,8 @@
 more information about this buildbot.
 
 
-@node Listing Change Sources and Schedulers, Setting the slaveport, Defining the Project, Configuration
-@section Listing Change Sources and Schedulers
+@node Change Sources and Schedulers, Setting the slaveport, Defining the Project, Configuration
+@section Change Sources and Schedulers
 
 @bcindex c['sources']
 @bcindex c['change_source']
@@ -2129,37 +2182,89 @@
 from buildbot.changes.pb import PBChangeSource
 c['change_source'] = PBChangeSource()
 @end example
+@bcindex c['schedulers']
 
 (note: in buildbot-0.7.5 and earlier, this key was named
 @code{c['sources']}, and required a list. @code{c['sources']} is
 deprecated as of buildbot-0.7.6 and is scheduled to be removed in a
 future release).
 
-@bcindex c['schedulers']
-@code{c['schedulers']} is a list of Scheduler instances, each of which
-causes builds to be started on a particular set of Builders. The two
-basic Scheduler classes you are likely to start with are
-@code{Scheduler} and @code{Periodic}, but you can write a customized
-subclass to implement more complicated build scheduling.
-
-The docstring for @code{buildbot.scheduler.Scheduler} is the best
-place to see all the options that can be used. Type @code{pydoc
-buildbot.scheduler.Scheduler} to see it, or look in
-@file{buildbot/scheduler.py} directly.
+@code{c['schedulers']} is a list of Scheduler instances, each
+of which causes builds to be started on a particular set of
+Builders. The two basic Scheduler classes you are likely to start
+with are @code{Scheduler} and @code{Periodic}, but you can write a
+customized subclass to implement more complicated build scheduling.
 
-The basic Scheduler takes four arguments:
+Scheduler arguments
+should always be specified by name (as keyword arguments), to allow
+for future expansion:
+
+@example
+sched = Scheduler(name="quick", builderNames=['lin', 'win'])
+@end example
+
+All schedulers have several arguments in common:  
 
 @table @code
 @item name
-Each Scheduler must have a unique name. This is only used in status
-displays.
+
+Each Scheduler must have a unique name. This is used in status
+displays, and is also available in the build property @code{scheduler}.
+
+@item builderNames
+
+This is the set of builders which this scheduler should trigger, specified
+as a list of names (strings).
+
+@item properties
+@cindex Properties
+
+This is a dictionary specifying properties that will be transmitted
+to all builds started by this scheduler.
+
+@end table
+
+Here is a brief catalog of the available Scheduler types. All these
+Schedulers are classes in @code{buildbot.scheduler}, and the
+docstrings there are the best source of documentation on the arguments
+taken by each one.
+
+@menu
+* Scheduler Scheduler::             
+* AnyBranchScheduler::          
+* Dependent Scheduler::          
+* Periodic Scheduler::          
+* Nightly Scheduler::          
+* Try Schedulers::          
+* Triggerable Scheduler::          
+@end menu
+
+@node Scheduler Scheduler
+@subsection Scheduler Scheduler
+@slindex buildbot.scheduler.Scheduler
+
+This is the original and still most popular Scheduler class. It follows
+exactly one branch, and starts a configurable tree-stable-timer after
+each change on that branch. When the timer expires, it starts a build
+on some set of Builders. The Scheduler accepts a @code{fileIsImportant}
+function which can be used to ignore some Changes if they do not
+affect any ``important'' files.
+
+The arguments to this scheduler are:
+
+@table @code
+@item name
+
+@item builderNames
+
+@item properties
 
 @item branch
 This Scheduler will pay attention to a single branch, ignoring Changes
 that occur on other branches. Setting @code{branch} equal to the
-special value of @code{None} means it should only pay attention to the
-default branch. Note that @code{None} is a keyword, not a string, so
-you want to use @code{None} and not @code{"None"}.
+special value of @code{None} means it should only pay attention to
+the default branch. Note that @code{None} is a keyword, not a string,
+so you want to use @code{None} and not @code{"None"}.
 
 @item treeStableTimer
 The Scheduler will wait for this many seconds before starting the
@@ -2167,108 +2272,78 @@
 restarted, so really the build will be started after a change and then
 after this many seconds of inactivity.
 
-@item builderNames
-When the tree-stable-timer finally expires, builds will be started on
-these Builders. Each Builder gets a unique name: these strings must
-match.
-
+@item fileIsImportant
+A callable which takes one argument, a Change instance, and returns
+@code{True} if the change is worth building, and @code{False} if
+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.
 @end table
 
+Example:
+
 @example
 from buildbot import scheduler
-quick = scheduler.Scheduler("quick", None, 60,
-                            ["quick-linux", "quick-netbsd"])
-full = scheduler.Scheduler("full", None, 5*60,
-                           ["full-linux", "full-netbsd", "full-OSX"])
-nightly = scheduler.Periodic("nightly", ["full-solaris"], 24*60*60)
-c['schedulers'] = [quick, full, nightly]
+quick = scheduler.Scheduler(name="quick", 
+                    branch=None, 
+                    treeStableTimer=60,
+                    builderNames=["quick-linux", "quick-netbsd"])
+full = scheduler.Scheduler(name="full", 
+                    branch=None,
+                    treeStableTimer=5*60,
+                    builderNames=["full-linux", "full-netbsd", "full-OSX"])
+c['schedulers'] = [quick, full]
 @end example
 
-In this example, the two ``quick'' builds are triggered 60 seconds
+In this example, the two ``quick'' builders are triggered 60 seconds
 after the tree has been changed. The ``full'' builds do not run quite
 so quickly (they wait 5 minutes), so hopefully if the quick builds
 fail due to a missing file or really simple typo, the developer can
 discover and fix the problem before the full builds are started. Both
-Schedulers only pay attention to the default branch: any changes on
-other branches are ignored by these Schedulers. Each Scheduler
+Schedulers only pay attention to the default branch: any changes
+on other branches are ignored by these Schedulers. Each Scheduler
 triggers a different set of Builders, referenced by name.
 
-The third Scheduler in this example just runs the full solaris build
-once per day. (note that this Scheduler only lets you control the time
-between builds, not the absolute time-of-day of each Build, so this
-could easily wind up a ``daily'' or ``every afternoon'' scheduler
-depending upon when it was first activated).
-
-@menu
-* Scheduler Types::             
-* Build Dependencies::          
-@end menu
-
-@node Scheduler Types, Build Dependencies, Listing Change Sources and Schedulers, Listing Change Sources and Schedulers
-@subsection Scheduler Types
-
-@slindex buildbot.scheduler.Scheduler
+@node AnyBranchScheduler
+@subsection AnyBranchScheduler
 @slindex buildbot.scheduler.AnyBranchScheduler
-@slindex buildbot.scheduler.Periodic
-@slindex buildbot.scheduler.Nightly
 
-Here is a brief catalog of the available Scheduler types. All these
-Schedulers are classes in @code{buildbot.scheduler}, and the
-docstrings there are the best source of documentation on the arguments
-taken by each one.
-
-@table @code
-@item Scheduler
-This is the default Scheduler class. It follows exactly one branch,
-and starts a configurable tree-stable-timer after each change on that
-branch. When the timer expires, it starts a build on some set of
-Builders. The Scheduler accepts a @code{fileIsImportant} function
-which can be used to ignore some Changes if they do not affect any
-``important'' files.
-
-@item AnyBranchScheduler
 This scheduler uses a tree-stable-timer like the default one, but
 follows multiple branches at once. Each branch gets a separate timer.
 
-@item Dependent
-This scheduler watches an ``upstream'' Scheduler. When all the
-Builders launched by that Scheduler successfully finish, the Dependent
-scheduler is triggered. The next section (@pxref{Build Dependencies})
-describes this scheduler in more detail.
-
-@item Triggerable
-This scheduler does nothing until it is triggered by a Trigger
-step in another build.  This facilitates a more general form of
-build dependencies, as described in the next section (@pxref{Build
-Dependencies}).
+The arguments to this scheduler are:
 
-@item Periodic
-This simple scheduler just triggers a build every N seconds.
+@table @code
+@item name
 
-@item Nightly
-This is highly configurable periodic build scheduler, which triggers a
-build at particular times of day, week, month, or year. The
-configuration syntax is very similar to the well-known @code{crontab}
-format, in which you provide values for minute, hour, day, and month
-(some of which can be wildcards), and a build is triggered whenever
-the current time matches the given constraints. This can run a build
-every night, every morning, every weekend, alternate Thursdays, on
-your boss's birthday, etc.
+@item builderNames
 
-@item Try_Jobdir / Try_Userpass
-This scheduler allows developers to use the @code{buildbot try}
-command to trigger builds of code they have not yet committed. See
-@ref{try} for complete details.
+@item properties
 
-@end table
+@item branches
+This Scheduler will pay attention to any number of branches, ignoring
+Changes that occur on other branches. Branches are specified just as
+for the @code{Scheduler} class.
 
-@node Build Dependencies,  , Scheduler Types, Listing Change Sources and Schedulers
-@subsection Build Dependencies
+@item treeStableTimer
+The Scheduler will wait for this many seconds before starting the
+build. If new changes are made during this interval, the timer will be
+restarted, so really the build will be started after a change and then
+after this many seconds of inactivity.
+
+@item fileIsImportant
+A callable which takes one argument, a Change instance, and returns
+@code{True} if the change is worth building, and @code{False} if
+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.
+@end table
 
+@node Dependent Scheduler
+@subsection Dependent Scheduler
 @cindex Dependent
 @cindex Dependencies
 @slindex buildbot.scheduler.Dependent
-@slindex buildbot.scheduler.Triggerable
 
 It is common to wind up with one kind of build which should only be
 performed if the same source code was successfully handled by some
@@ -2284,61 +2359,239 @@
 that is used by some other Builder, you'd want to make sure the
 consuming Build is run @emph{after} the producing one.
 
-You can use @code{Dependencies} to express this relationship to the
-Buildbot. There is a special kind of Scheduler named
+You can use ``Dependencies'' to express this relationship
+to the Buildbot. There is a special kind of Scheduler named
 @code{scheduler.Dependent} that will watch an ``upstream'' Scheduler
-for builds to complete successfully (on all of its Builders). Each
-time that happens, the same source code (i.e. the same
-@code{SourceStamp}) will be used to start a new set of builds, on a
-different set of Builders. This ``downstream'' scheduler doesn't pay
-attention to Changes at all, it only pays attention to the upstream
-scheduler.
+for builds to complete successfully (on all of its Builders). Each time
+that happens, the same source code (i.e. the same @code{SourceStamp})
+will be used to start a new set of builds, on a different set of
+Builders. This ``downstream'' scheduler doesn't pay attention to
+Changes at all. It only pays attention to the upstream scheduler.
+
+If the build fails on any of the Builders in the upstream set,
+the downstream builds will not fire.  Note that, for SourceStamps
+generated by a ChangeSource, the @code{revision} is None, meaning HEAD.
+If any changes are committed between the time the upstream scheduler
+begins its build and the time the dependent scheduler begins its
+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:
+
+@table @code
+@item name
+
+@item builderNames
+
+@item properties
+
+@item upstream
+The upstream scheduler to watch.  Note that this is an ``instance'',
+not the name of the scheduler.
+@end table
 
-If the SourceStamp fails on any of the Builders in the upstream set,
-the downstream builds will not fire.
+Example:
 
 @example
 from buildbot import scheduler
-tests = scheduler.Scheduler("tests", None, 5*60,
+tests = scheduler.Scheduler("just-tests", None, 5*60,
                             ["full-linux", "full-netbsd", "full-OSX"])
-package = scheduler.Dependent("package",
-                              tests, # upstream scheduler
+package = scheduler.Dependent("build-package",
+                              tests, # upstream scheduler -- no quotes!
                               ["make-tarball", "make-deb", "make-rpm"])
 c['schedulers'] = [tests, package]
 @end example
 
-Note that @code{Dependent}'s upstream scheduler argument is given as a
-@code{Scheduler} @emph{instance}, not a name. This makes it impossible
-to create circular dependencies in the config file.
-
-A more general way to coordinate builds is by ``triggering''
-schedulers from builds. The Triggerable waits to be triggered by a
-Trigger step (@pxref{Triggering Schedulers}) in another build. That
-step can optionally wait for the scheduler's builds to complete. This
+@node Periodic Scheduler
+@subsection Periodic Scheduler
+@slindex buildbot.scheduler.Periodic
+
+This simple scheduler just triggers a build every N seconds.
+
+The arguments to this scheduler are:
+
+@table @code
+@item name
+
+@item builderNames
+
+@item properties
+
+@item periodicBuildTimer
+The time, in seconds, after which to start a build.
+@end table
+
+Example:
+
+@example
+from buildbot import scheduler
+nightly = scheduler.Periodic(name="nightly",
+                builderNames=["full-solaris"],
+                periodicBuildTimer=24*60*60)
+c['schedulers'] = [nightly]
+@end example
+
+The Scheduler in this example just runs the full solaris build once
+per day. Note that this Scheduler only lets you control the time
+between builds, not the absolute time-of-day of each Build, so this
+could easily wind up a ``daily'' or ``every afternoon'' scheduler
+depending upon when it was first activated.
+
+@node Nightly Scheduler
+@subsection Nightly Scheduler
+@slindex buildbot.scheduler.Nightly
+
+This is highly configurable periodic build scheduler, which triggers
+a build at particular times of day, week, month, or year. The
+configuration syntax is very similar to the well-known @code{crontab}
+format, in which you provide values for minute, hour, day, and month
+(some of which can be wildcards), and a build is triggered whenever
+the current time matches the given constraints. This can run a build
+every night, every morning, every weekend, alternate Thursdays,
+on your boss's birthday, etc.
+
+Pass some subset of @code{minute}, @code{hour}, @code{dayOfMonth},
+@code{month}, and @code{dayOfWeek}; each may be a single number or
+a list of valid values. The builds will be triggered whenever the
+current time matches these values. Wildcards are represented by a
+'*' string. All fields default to a wildcard except 'minute', so
+with no fields this defaults to a build every hour, on the hour.
+The full list of parameters is:
+
+@table @code
+@item name
+
+@item builderNames
+
+@item properties
+
+@item branch
+The branch to build, just as for @code{Scheduler}.
+
+@item minute
+The minute of the hour on which to start the build.  This defaults
+to 0, meaning an hourly build.
+
+@item hour
+The hour of the day on which to start the build, in 24-hour notation.
+This defaults to *, meaning every hour.
+
+@item month
+The month in which to start the build, with January = 1.  This defaults
+to *, meaning every month.
+
+@item dayOfWeek
+The day of the week to start a build, with Monday = 0.  This defauls
+to *, meaning every day of the week.
+@end table
+
+For example, the following master.cfg clause will cause a build to be
+started every night at 3:00am:
+
+@example
+s = scheduler.Nightly(name='nightly',
+        builderNames=['builder1', 'builder2'],
+        hour=3, 
+        minute=0)
+@end example
+
+This scheduler will perform a build each monday morning at 6:23am and
+again at 8:23am:
+
+@example
+s = scheduler.Nightly(name='BeforeWork',
+         builderNames=['builder1'],
+         dayOfWeek=0,
+         hour=[6,8],
+         minute=23)
+@end example
+
+The following runs a build every two hours, using Python's @code{range}
+function:
+
+@example
+s = Nightly(name='every2hours',
+        builderNames=['builder1'],
+        hour=range(0, 24, 2))
+@end example
+
+Finally, this example will run only on December 24th:
+
+@example
+s = Nightly(name='SleighPreflightCheck',
+        builderNames=['flying_circuits', 'radar'],
+        month=12,
+        dayOfMonth=24,
+        hour=12,
+        minute=0)
+@end example
+
+@node Try Schedulers
+@subsection Try Schedulers
+@slindex buildbot.scheduler.Try_Jobdir
+@slindex buildbot.scheduler.Try_Userpass
+
+This scheduler allows developers to use the @code{buildbot try}
+command to trigger builds of code they have not yet committed. See
+@ref{try} for complete details.
+
+Two implementations are available: @code{Try_Jobdir} and
+@code{Try_Userpass}.  The former monitors a job directory, specified
+by the @code{jobdir} parameter, while the latter listens for PB 
+connections on a specific @code{port}, and authenticates against
+@code{userport}.
+
+@node Triggerable Scheduler
+@subsection Triggerable Scheduler
+@cindex Triggers
+@slindex buildbot.scheduler.Triggerable
+
+The @code{Triggerable} scheduler waits to be triggered by a Trigger
+step (see @ref{Triggering Schedulers}) in another build. That step
+can optionally wait for the scheduler's builds to complete. This
 provides two advantages over Dependent schedulers. First, the same
 scheduler can be triggered from multiple builds. Second, the ability
 to wait for a Triggerable's builds to complete provides a form of
-"subroutine call", where one or more builds can "call" a scheduler to
-perform some work for them, perhaps on other buildslaves.
+"subroutine call", where one or more builds can "call" a scheduler
+to perform some work for them, perhaps on other buildslaves.
+
+The parameters are just the basics:
+
+@table @code
+@item name
+@item builderNames
+@item properties
+@end table
+
+This class is only useful in conjunction with the @code{Trigger} step.
+Here is a fully-worked example:
 
 @example
 from buildbot import scheduler
 from buildbot.steps import trigger
 
-checkin = scheduler.Scheduler("checkin", None, 5*60, ["checkin"])
-nightly = scheduler.Scheduler("nightly", ... , ["nightly"])
-
-mktarball = scheduler.Triggerable("mktarball",
-                                  ["mktarball"])
-build = scheduler.Triggerable("build-all-platforms",
-                              ["build-all-platforms"])
-test = scheduler.Triggerable("distributed-test",
-                             ["distributed-test"])
-package = scheduler.Triggerable("package-all-platforms",
-                                ["package-all-platforms"])
+checkin = scheduler.Scheduler(name="checkin",
+            branch=None,
+            treeStableTimer=5*60,
+            builderNames=["checkin"])
+nightly = scheduler.Nightly(name='nightly',
+            builderNames=['nightly'],
+            hour=3, 
+            minute=0)
+
+mktarball = scheduler.Triggerable(name="mktarball",
+                builderNames=["mktarball"])
+build = scheduler.Triggerable(name="build-all-platforms",
+                builderNames=["build-all-platforms"])
+test = scheduler.Triggerable(name="distributed-test",
+                builderNames=["distributed-test"])
+package = scheduler.Triggerable(name="package-all-platforms",
+                builderNames=["package-all-platforms"])
 
 c['schedulers'] = [checkin, nightly, build, test, package]
 
+# on checkin, make a tarball, build it, and test it
 checkin_factory = factory.BuildFactory()
 f.addStep(trigger.Trigger('mktarball', schedulers=['mktarball'],
                                        waitForFinish=True)
@@ -2347,6 +2600,7 @@
 f.addStep(trigger.Trigger('test', schedulers=['distributed-test'],
                                   waitForFinish=True)
 
+# and every night, make a tarball, build it, and package it
 nightly_factory = factory.BuildFactory()
 f.addStep(trigger.Trigger('mktarball', schedulers=['mktarball'],
                                        waitForFinish=True)
@@ -2356,7 +2610,7 @@
                                      waitForFinish=True)
 @end example
 
-@node Setting the slaveport, Buildslave Specifiers, Listing Change Sources and Schedulers, Configuration
+@node Setting the slaveport, Buildslave Specifiers, Change Sources and Schedulers, Configuration
 @section Setting the slaveport
 
 @bcindex c['slavePortnum']
@@ -2393,7 +2647,7 @@
 @code{localhost:10000}.
 
 
-@node Buildslave Specifiers, Defining Builders, Setting the slaveport, Configuration
+@node Buildslave Specifiers
 @section Buildslave Specifiers
 
 @bcindex c['slaves']
@@ -2404,13 +2658,6 @@
 values that need to be provided to the buildslave administrator when
 they create the buildslave.
 
-@example
-from buildbot.buildslave import BuildSlave
-c['slaves'] = [BuildSlave('bot-solaris', 'solarispasswd'),
-               BuildSlave('bot-bsd', 'bsdpasswd'),
-              ]
-@end example
-
 The slavenames must be unique, of course. The password exists to
 prevent evildoers from interfering with the buildbot by inserting
 their own (broken) buildslaves into the system and thus displacing the
@@ -2420,9 +2667,28 @@
 will be rejected when they attempt to connect, and a message
 describing the problem will be put in the log file (see @ref{Logfiles}).
 
-The @code{BuildSlave} constructor can take an optional
-@code{max_builds} parameter to limit the number of builds that it will
-execute simultaneously:
+@example
+from buildbot.buildslave import BuildSlave
+c['slaves'] = [BuildSlave('bot-solaris', 'solarispasswd')
+               BuildSlave('bot-bsd', 'bsdpasswd')
+              ]
+@end example
+
+@cindex Properties
+@code{BuildSlave} objects can also be created with an optional
+@code{properties} argument, a dictionary specifying properties that
+will be available to any builds performed on this slave.  For example:
+
+@example
+from buildbot.buildslave import BuildSlave
+c['slaves'] = [BuildSlave('bot-solaris', 'solarispasswd',
+                    properties=@{'os':'solaris'@}),
+              ]
+@end example
+
+The @code{BuildSlave} constructor can also take an optional
+@code{max_builds} parameter to limit the number of builds that it
+will execute simultaneously:
 
 @example
 from buildbot.buildslave import BuildSlave
@@ -2496,8 +2762,23 @@
               ]
 @end example
 
+@node Defining Global Properties
+@section Defining Global Properties
+@bcindex c['properties']
+@cindex Properties
+
+The @code{'properties'} configuration key defines a dictionary
+of properties that will be available to all builds started by the
+buildmaster:
 
-@node Defining Builders, Defining Status Targets, Buildslave Specifiers, Configuration
+@example
+c['properties'] = @{
+    'Widget-version' : '1.2',
+    'release-stage' : 'alpha'
+@}
+@end example
+
+@node Defining Builders
 @section Defining Builders
 
 @bcindex c['builders']
@@ -2577,7 +2858,7 @@
 @end table
 
 
-@node Defining Status Targets, Debug options, Defining Builders, Configuration
+@node Defining Status Targets
 @section Defining Status Targets
 
 The Buildmaster has a variety of ways to present build status to
@@ -2969,8 +3250,8 @@
 in a place where the buildmaster can find them, and configure the
 buildmaster to parse the messages correctly. Once that is in place,
 the email parser will create Change objects and deliver them to the
-Schedulers (see @pxref{Scheduler Types}) just like any other
-ChangeSource.
+Schedulers (see @pxref{Change Sources and Schedulers}) just
+like any other ChangeSource.
 
 There are two components to setting up an email-based ChangeSource.
 The first is to route the email messages to the buildmaster, which is
@@ -3721,7 +4002,7 @@
 * Build Factories::             
 @end menu
 
-@node Build Steps, Interlocks, Build Process, Build Process
+@node Build Steps
 @section Build Steps
 
 @code{BuildStep}s are usually specified in the buildmaster's
@@ -3756,6 +4037,7 @@
 
 @menu
 * Common Parameters::           
+* Using Build Properties::
 * Source Checkout::             
 * ShellCommand::                
 * Simple ShellCommand Subclasses::  
@@ -3765,7 +4047,7 @@
 * Writing New BuildSteps::      
 @end menu
 
-@node Common Parameters, Source Checkout, Build Steps, Build Steps
+@node Common Parameters
 @subsection Common Parameters
 
 The standard @code{Build} runs a series of @code{BuildStep}s in order,
@@ -3816,8 +4098,163 @@
 
 @end table
 
+@node Using Build Properties
+@subsection Using Build Properties
+@cindex Properties
+
+Build properties are a generalized way to provide configuration
+information to build steps; see @ref{Build Properties}.
+
+Some build properties are inherited from external sources -- global
+properties, schedulers, or buildslaves.  Some build properties are
+set when the build starts, such as the SourceStamp information. Other
+properties can be set by BuildSteps as they run, for example the
+various Source steps will set the @code{got_revision} property to the
+source revision that was actually checked out (which can be useful
+when the SourceStamp in use merely requested the ``latest revision'':
+@code{got_revision} will tell you what was actually built).
+
+In custom BuildSteps, you can get and set the build properties with
+the @code{getProperty}/@code{setProperty} methods. Each takes a string
+for the name of the property, and returns or accepts an
+arbitrary@footnote{Build properties are serialized along with the
+build results, so they must be serializable. For this reason, the
+value of any build property should be simple inert data: strings,
+numbers, lists, tuples, and dictionaries. They should not contain
+class instances.} object. For example:
+
+@example
+class MakeTarball(ShellCommand):
+    def start(self):
+        if self.getProperty("os") == "win":
+            self.setCommand([ ... ]) # windows-only command
+        else:
+            self.setCommand([ ... ]) # equivalent for other systems
+        ShellCommand.start(self)
+@end example
+
+@heading WithProperties
+@cindex WithProperties
+
+You can use build properties in ShellCommands by using the
+@code{WithProperties} wrapper when setting the arguments of the
+ShellCommand. This interpolates the named build properties into the
+generated shell command. You can also use a @code{WithProperties}
+as the @code{workdir=} argument: this allows the working directory
+for a command to be varied for each build, depending upon various
+build properties.
+
+@example
+from buildbot.steps.shell import ShellCommand
+from buildbot.process.properties import WithProperties
+
+f.addStep(ShellCommand,
+          command=["tar", "czf",
+                   WithProperties("build-%s.tar.gz", "revision"),
+                   "source"])
+@end example
+
+If this BuildStep were used in a tree obtained from Subversion, it
+would create a tarball with a name like @file{build-1234.tar.gz}.
+
+The @code{WithProperties} function does @code{printf}-style string
+interpolation, using strings obtained by calling
+@code{build.getProperty(propname)}. Note that for every @code{%s} (or
+@code{%d}, etc), you must have exactly one additional argument to
+indicate which build property you want to insert.
+
+You can also use python dictionary-style string interpolation by using
+the @code{%(propname)s} syntax. In this form, the property name goes
+in the parentheses, and WithProperties takes @emph{no} additional
+arguments:
+
+@example
+f.addStep(ShellCommand,
+          command=["tar", "czf",
+                   WithProperties("build-%(revision)s.tar.gz"),
+                   "source"])
+@end example
+
+Don't forget the extra ``s'' after the closing parenthesis! This is
+the cause of many confusing errors. 
+
+Note that, like python, you can either do positional-argument
+interpolation @emph{or} keyword-argument interpolation, not both. Thus
+you cannot use a string like
+@code{WithProperties("foo-%(revision)s-%s", "branch")}.
+
+Most step parameters accept @code{WithProperties}.  Please file bugs
+for any parameters which do not.
+
+@heading Common Build Properties
+
+The following build properties are set when the build is started, and
+are available to all steps.
+
+@table @code
+@item branch
+
+This comes from the build's SourceStamp, and describes which branch is
+being checked out. This will be @code{None} (which interpolates into
+@code{WithProperties} as an empty string) if the build is on the
+default branch, which is generally the trunk. Otherwise it will be a
+string like ``branches/beta1.4''. The exact syntax depends upon the VC
+system being used.
+
+@item revision
+
+This also comes from the SourceStamp, and is the revision of the
+source code tree that was requested from the VC system. When a build
+is requested of a specific revision (as is generally the case when
+the build is triggered by Changes), this will contain the revision
+specification. The syntax depends upon the VC system in use: for SVN
+it is an integer, for Mercurial it is a short string, for Darcs it
+is a rather large string, etc.
+
+If the ``force build'' button was pressed, the revision will be
+@code{None}, which means to use the most recent revision available.
+This is a ``trunk build''. This will be interpolated as an empty
+string.
+
+@item got_revision
+
+This is set when a Source step checks out the source tree, and
+provides the revision that was actually obtained from the VC system.
+In general this should be the same as @code{revision}, except for
+trunk builds, where @code{got_revision} indicates what revision was
+current when the checkout was performed. This can be used to rebuild
+the same source code later.
+
+Note that for some VC systems (Darcs in particular), the revision is a
+large string containing newlines, and is not suitable for interpolation
+into a filename.
+
+@item buildername
+
+This is a string that indicates which Builder the build was a part of.
+The combination of buildername and buildnumber uniquely identify a
+build.
+
+@item buildnumber
+
+Each build gets a number, scoped to the Builder (so the first build
+performed on any given Builder will have a build number of 0). This
+integer property contains the build's number.
+
+@item slavename
+
+This is a string which identifies which buildslave the build is
+running on.
+
+@item scheduler
+
+If the build was started from a scheduler, then this property will
+contain the name of that scheduler.
+
+@end table
+
 
-@node Source Checkout, ShellCommand, Common Parameters, Build Steps
+@node Source Checkout
 @subsection Source Checkout
 
 The first step of any build is typically to acquire the source code
@@ -4475,7 +4912,6 @@
 * Compile::                     
 * Test::                        
 * TreeSize::                    
-* Build Properties::            
 @end menu
 
 @node Configure, Compile, Simple ShellCommand Subclasses, Simple ShellCommand Subclasses
@@ -4531,7 +4967,7 @@
 This is meant to handle unit tests. The default command is @code{make
 test}, and the @code{warnOnFailure} flag is set.
 
-@node TreeSize, Build Properties, Test, Simple ShellCommand Subclasses
+@node TreeSize, , Test, Simple ShellCommand Subclasses
 @subsubsection TreeSize
 
 @bsindex buildbot.steps.shell.TreeSize
@@ -4542,177 +4978,6 @@
 property named 'tree-size-KiB' with the same value.
 
 
-@node Build Properties,  , TreeSize, Simple ShellCommand Subclasses
-@subsubsection Build Properties
-
-@cindex build properties
-
-Each build has a set of ``Build Properties'', which can be used by its
-BuildStep to modify their actions. For example, the SVN revision
-number of the source code being built is available as a build
-property, and a ShellCommand step could incorporate this number into a
-command which create a numbered release tarball.
-
-Some build properties are set when the build starts, such as the
-SourceStamp information. Other properties can be set by BuildSteps as
-they run, for example the various Source steps will set the
-@code{got_revision} property to the source revision that was actually
-checked out (which can be useful when the SourceStamp in use merely
-requested the ``latest revision'': @code{got_revision} will tell you
-what was actually built).
-
-@itemize
-@item buildername
-Name of this builder
-@item buildnumber
-Number of this build (numbers are unique within the builder)
-@item branch
-Branch of the source being built, from the SourceStamp
-@item revision
-Revision of the source being built, from the SourceStamp, as a string
-@item scheduler
-The name of the scheduler that invoked this build
-@item slavename
-The name of the buildslave performing this build
-@end itemize
-
-In custom BuildSteps, you can get and set the build properties with
-the @code{getProperty}/@code{setProperty} methods. Each takes a string
-for the name of the property, and returns or accepts an
-arbitrary@footnote{Build properties are serialized along with the
-build results, so they must be serializable. For this reason, the
-value of any build property should be simple inert data: strings,
-numbers, lists, tuples, and dictionaries. They should not contain
-class instances.} object. For example:
-
-@example
-class MakeTarball(ShellCommand):
-    def start(self):
-        self.setCommand(["tar", "czf",
-                         "build-%s.tar.gz" % self.getProperty("revision"),
-                         "source"])
-        ShellCommand.start(self)
-@end example
-
-@cindex WithProperties
-
-You can use build properties in ShellCommands by using the
-@code{WithProperties} wrapper when setting the arguments of the
-ShellCommand. This interpolates the named build properties into the
-generated shell command. You can also use a @code{WithProperties} as
-the @code{workdir=} argument: this allows the working directory for a
-command to be varied for each build, depending upon various build
-properties.
-
-@example
-from buildbot.steps.shell import ShellCommand, WithProperties
-
-f.addStep(ShellCommand,
-          command=["tar", "czf",
-                   WithProperties("build-%s.tar.gz", "revision"),
-                   "source"])
-@end example
-
-If this BuildStep were used in a tree obtained from Subversion, it
-would create a tarball with a name like @file{build-1234.tar.gz}.
-
-The @code{WithProperties} function does @code{printf}-style string
-interpolation, using strings obtained by calling
-@code{build.getProperty(propname)}. Note that for every @code{%s} (or
-@code{%d}, etc), you must have exactly one additional argument to
-indicate which build property you want to insert.
-
-
-You can also use python dictionary-style string interpolation by using
-the @code{%(propname)s} syntax. In this form, the property name goes
-in the parentheses, and WithProperties takes @emph{no} additional
-arguments:
-
-@example
-f.addStep(ShellCommand,
-          command=["tar", "czf",
-                   WithProperties("build-%(revision)s.tar.gz"),
-                   "source"])
-@end example
-
-Don't forget the extra ``s'' after the closing parenthesis! This is
-the cause of many confusing errors. Also note that you can only use
-WithProperties in the list form of the command= definition. You cannot
-currently use it in the (discouraged) @code{command="stuff"}
-single-string form. However, you can use something like
-@code{command=["/bin/sh", "-c", "stuff", WithProperties(stuff)]} to
-use both shell expansion and WithProperties interpolation.
-
-Note that, like python, you can either do positional-argument
-interpolation @emph{or} keyword-argument interpolation, not both. Thus
-you cannot use a string like
-@code{WithProperties("foo-%(revision)s-%s", "branch")}.
-
-At the moment, the only way to set build properties is by writing a
-custom BuildStep.
-
-@heading Common Build Properties
-
-The following build properties are set when the build is started, and
-are available to all steps.
-
-@table @code
-@item branch
-
-This comes from the build's SourceStamp, and describes which branch is
-being checked out. This will be @code{None} (which interpolates into
-@code{WithProperties} as an empty string) if the build is on the
-default branch, which is generally the trunk. Otherwise it will be a
-string like ``branches/beta1.4''. The exact syntax depends upon the VC
-system being used.
-
-@item revision
-
-This also comes from the SourceStamp, and is the revision of the
-source code tree that was requested from the VC system. When a build
-is requested of a specific revision (as is generally the case when the
-build is triggered by Changes), this will contain the revision
-specification. The syntax depends upon the VC system in use: for SVN
-it is an integer, for Mercurial it is a short string, for Darcs it is
-a rather large string, etc.
-
-If the ``force build'' button was pressed, the revision will be
-@code{None}, which means to use the most recent revision available.
-This is a ``trunk build''. This will be interpolated as an empty
-string.
-
-@item got_revision
-
-This is set when a Source step checks out the source tree, and
-provides the revision that was actually obtained from the VC system.
-In general this should be the same as @code{revision}, except for
-trunk builds, where @code{got_revision} indicates what revision was
-current when the checkout was performed. This can be used to rebuild
-the same source code later.
-
-Note that for some VC systems (Darcs in particular), the revision is a
-large string containing newlines, and is not suitable for
-interpolation into a filename.
-
-@item buildername
-
-This is a string that indicates which Builder the build was a part of.
-The combination of buildername and buildnumber uniquely identify a
-build.
-
-@item buildnumber
-
-Each build gets a number, scoped to the Builder (so the first build
-performed on any given Builder will have a build number of 0). This
-integer property contains the build's number.
-
-@item slavename
-
-This is a string which identifies which buildslave the build is
-running on.
-
-@end table
-
 @node Python BuildSteps, Transferring Files, Simple ShellCommand Subclasses, Build Steps
 @subsection Python BuildSteps
 
@@ -4882,7 +5147,7 @@
 @subsection Triggering Schedulers
 
 The counterpart to the Triggerable described in section
-@pxref{Build Dependencies} is the Trigger BuildStep.
+@pxref{Triggerable Scheduler} is the Trigger BuildStep.
 
 @example
 from buildbot.steps.trigger import Trigger
@@ -5302,9 +5567,19 @@
 [GCC 4.1.2 20060928 (prerelease) (Debian 4.1.1-15)] on linux2
 Type "help", "copyright", "credits" or "license" for more information.
 >>> import sys
->>> print sys.path
-['', '/usr/lib/python24.zip', '/usr/lib/python2.4', '/usr/lib/python2.4/plat-linux2', '/usr/lib/python2.4/lib-tk', '/usr/lib/python2.4/lib-dynload', '/usr/local/lib/python2.4/site-packages', '/usr/lib/python2.4/site-packages', '/usr/lib/python2.4/site-packages/Numeric', '/var/lib/python-support/python2.4', '/usr/lib/site-python']
->>> 
+>>> import pprint
+>>> pprint.pprint(sys.path)
+['',
+ '/usr/lib/python24.zip',
+ '/usr/lib/python2.4',
+ '/usr/lib/python2.4/plat-linux2',
+ '/usr/lib/python2.4/lib-tk',
+ '/usr/lib/python2.4/lib-dynload',
+ '/usr/local/lib/python2.4/site-packages',
+ '/usr/lib/python2.4/site-packages',
+ '/usr/lib/python2.4/site-packages/Numeric',
+ '/var/lib/python-support/python2.4',
+ '/usr/lib/site-python']
 @end example
 
 In this case, putting the code into


