3 from __future__
import generators
11 from twisted
.python
import log
12 from twisted
.internet
import defer
13 from twisted
.application
import service
14 from twisted
.web
import html
16 from buildbot
import interfaces
, util
17 from buildbot
.twcompat
import implements
, providedBy
20 <p>Changed by: <b>%(who)s</b><br />
21 Changed at: <b>%(at)s</b><br />
35 """I represent a single change to the source tree. This may involve
36 several files, but they are all changed by the same person, and there is
37 a change comment for the group as a whole.
39 If the version control system supports sequential repository- (or
40 branch-) wide change numbers (like SVN, P4, and Arch), then revision=
41 should be set to that number. The highest such number will be used at
42 checkout time to get the correct set of files.
44 If it does not (like CVS), when= should be set to the timestamp (seconds
45 since epoch, as returned by time.time()) when the change was made. when=
46 will be filled in for you (to the current time) if you omit it, which is
47 suitable for ChangeSources which have no way of getting more accurate
50 Changes should be submitted to ChangeMaster.addChange() in
51 chronologically increasing order. Out-of-order changes will probably
52 cause the html.Waterfall display to be corrupted."""
55 implements(interfaces
.IStatusEvent
)
57 __implements__
= interfaces
.IStatusEvent
,
63 revision
= None # used to create a source-stamp
65 def __init__(self
, who
, files
, comments
, isdir
=0, links
=[],
66 revision
=None, when
=None, branch
=None):
69 self
.comments
= comments
72 self
.revision
= revision
80 data
+= self
.getFileContents()
81 data
+= "At: %s\n" % self
.getTime()
82 data
+= "Changed By: %s\n" % self
.who
83 data
+= "Comments: %s\n\n" % self
.comments
88 for file in self
.files
:
89 link
= filter(lambda s
: s
.find(file) != -1, self
.links
)
92 links
.append('<a href="%s"><b>%s</b></a>' % (link
[0], file))
94 links
.append('<b>%s</b>' % file)
97 revision
= "Revision: <b>%s</b><br />\n" % self
.revision
100 branch
= "Branch: <b>%s</b><br />\n" % self
.branch
102 kwargs
= { 'who' : html
.escape(self
.who
),
103 'at' : self
.getTime(),
104 'files' : html
.UL(links
) + '\n',
105 'revision': revision
,
107 'comments': html
.PRE(self
.comments
) }
108 return html_tmpl
% kwargs
113 return time
.strftime("%a %d %b %Y %H:%M:%S",
114 time
.localtime(self
.when
))
117 return (self
.when
, None)
120 return [html
.escape(self
.who
)]
126 def getFileContents(self
):
128 if len(self
.files
) == 1:
130 data
+= "Directory: %s\n" % self
.files
[0]
132 data
+= "File: %s\n" % self
.files
[0]
139 class ChangeMaster(service
.MultiService
):
141 """This is the master-side service which receives file change
142 notifications from CVS. It keeps a log of these changes, enough to
143 provide for the HTML waterfall display, and to tell
144 temporarily-disconnected bots what they missed while they were
147 Change notifications come from two different kinds of sources. The first
148 is a PB service (servicename='changemaster', perspectivename='change'),
149 which provides a remote method called 'addChange', which should be
150 called with a dict that has keys 'filename' and 'comments'.
152 The second is a list of objects derived from the ChangeSource class.
153 These are added with .addSource(), which also sets the .changemaster
154 attribute in the source to point at the ChangeMaster. When the
155 application begins, these will be started with .start() . At shutdown
156 time, they will be terminated with .stop() . They must be persistable.
157 They are expected to call self.changemaster.addChange() with Change
160 There are several different variants of the second type of source:
162 - L{buildbot.changes.mail.MaildirSource} watches a maildir for CVS
163 commit mail. It uses DNotify if available, or polls every 10
164 seconds if not. It parses incoming mail to determine what files
167 - L{buildbot.changes.freshcvs.FreshCVSSource} makes a PB
168 connection to the CVSToys 'freshcvs' daemon and relays any
169 changes it announces.
174 # todo: use Maildir class to watch for changes arriving by mail
177 service
.MultiService
.__init
__(self
)
179 # self.basedir must be filled in by the parent
182 def addSource(self
, source
):
183 assert providedBy(source
, interfaces
.IChangeSource
)
184 assert providedBy(source
, service
.IService
)
186 print "ChangeMaster.addSource", source
187 source
.setServiceParent(self
)
189 def removeSource(self
, source
):
190 assert source
in self
192 print "ChangeMaster.removeSource", source
, source
.parent
193 d
= defer
.maybeDeferred(source
.disownServiceParent
)
196 def addChange(self
, change
):
197 """Deliver a file change event. The event should be a Change object.
198 This method will timestamp the object as it is received."""
199 log
.msg("adding change, who %s, %d files, rev=%s, branch=%s, "
200 "comments %s" % (change
.who
, len(change
.files
),
201 change
.revision
, change
.branch
,
203 change
.number
= self
.nextNumber
205 self
.changes
.append(change
)
206 self
.parent
.addChange(change
)
207 # TODO: call pruneChanges after a while
209 def pruneChanges(self
):
210 self
.changes
= self
.changes
[-100:] # or something
212 def eventGenerator(self
):
213 for i
in range(len(self
.changes
)-1, -1, -1):
217 def getChangeNumbered(self
, num
):
220 first
= self
.changes
[0].number
221 if first
+ len(self
.changes
)-1 != self
.changes
[-1].number
:
223 "lost a change somewhere: [0] is %d, [%d] is %d" % \
224 (self
.changes
[0].number
,
225 len(self
.changes
) - 1,
226 self
.changes
[-1].number
))
227 for c
in self
.changes
:
228 log
.msg("c[%d]: " % c
.number
, c
)
231 log
.msg(self
, "offset", offset
)
232 return self
.changes
[offset
]
234 def __getstate__(self
):
235 d
= service
.MultiService
.__getstate
__(self
)
237 del d
['services'] # lose all children
238 del d
['namedServices']
241 def __setstate__(self
, d
):
243 # self.basedir must be set by the parent
244 self
.services
= [] # they'll be repopulated by readConfig
245 self
.namedServices
= {}
248 def saveYourself(self
):
249 filename
= os
.path
.join(self
.basedir
, "changes.pck")
250 tmpfilename
= filename
+ ".tmp"
252 pickle
.dump(self
, open(tmpfilename
, "wb"))
253 if sys
.platform
== 'win32':
254 # windows cannot rename a file on top of an existing one
255 if os
.path
.exists(filename
):
257 os
.rename(tmpfilename
, filename
)
259 log
.msg("unable to save changes")
262 def stopService(self
):
264 return service
.MultiService
.stopService(self
)