Package buildbot :: Package changes :: Module changes
[frames] | no frames]

Source Code for Module buildbot.changes.changes

  1  import sys, os, time 
  2  from cPickle import dump 
  3   
  4  from zope.interface import implements 
  5  from twisted.python import log 
  6  from twisted.internet import defer 
  7  from twisted.application import service 
  8  from twisted.web import html 
  9   
 10  from buildbot import interfaces, util 
 11  from buildbot.process.properties import Properties 
 12   
 13  html_tmpl = """ 
 14  <p>Changed by: <b>%(who)s</b><br /> 
 15  Changed at: <b>%(at)s</b><br /> 
 16  %(branch)s 
 17  %(revision)s 
 18  <br /> 
 19   
 20  Changed files: 
 21  %(files)s 
 22   
 23  Comments: 
 24  %(comments)s 
 25   
 26  Properties: 
 27  %(properties)s 
 28  </p> 
 29  """ 
 30   
31 -class Change:
32 """I represent a single change to the source tree. This may involve 33 several files, but they are all changed by the same person, and there is 34 a change comment for the group as a whole. 35 36 If the version control system supports sequential repository- (or 37 branch-) wide change numbers (like SVN, P4, and Arch), then revision= 38 should be set to that number. The highest such number will be used at 39 checkout time to get the correct set of files. 40 41 If it does not (like CVS), when= should be set to the timestamp (seconds 42 since epoch, as returned by time.time()) when the change was made. when= 43 will be filled in for you (to the current time) if you omit it, which is 44 suitable for ChangeSources which have no way of getting more accurate 45 timestamps. 46 47 Changes should be submitted to ChangeMaster.addChange() in 48 chronologically increasing order. Out-of-order changes will probably 49 cause the html.Waterfall display to be corrupted.""" 50 51 implements(interfaces.IStatusEvent) 52 53 number = None 54 55 branch = None 56 category = None 57 revision = None # used to create a source-stamp 58
59 - def __init__(self, who, files, comments, isdir=0, links=None, 60 revision=None, when=None, branch=None, category=None, 61 revlink='', properties={}):
62 self.who = who 63 self.comments = comments 64 self.isdir = isdir 65 if links is None: 66 links = [] 67 self.links = links 68 self.revision = revision 69 if when is None: 70 when = util.now() 71 self.when = when 72 self.branch = branch 73 self.category = category 74 self.revlink = revlink 75 self.properties = Properties() 76 self.properties.update(properties, "Change") 77 78 # keep a sorted list of the files, for easier display 79 self.files = files[:] 80 self.files.sort()
81
82 - def __setstate__(self, dict):
83 self.__dict__ = dict 84 # Older Changes won't have a 'properties' attribute in them 85 if not hasattr(self, 'properties'): 86 self.properties = Properties()
87
88 - def asText(self):
89 data = "" 90 data += self.getFileContents() 91 data += "At: %s\n" % self.getTime() 92 data += "Changed By: %s\n" % self.who 93 data += "Comments: %s" % self.comments 94 data += "Properties: \n%s\n\n" % self.getProperties() 95 return data
96
97 - def asHTML(self):
98 links = [] 99 for file in self.files: 100 link = filter(lambda s: s.find(file) != -1, self.links) 101 if len(link) == 1: 102 # could get confused 103 links.append('<a href="%s"><b>%s</b></a>' % (link[0], file)) 104 else: 105 links.append('<b>%s</b>' % file) 106 if self.revision: 107 if getattr(self, 'revlink', ""): 108 revision = 'Revision: <a href="%s"><b>%s</b></a>\n' % ( 109 self.revlink, self.revision) 110 else: 111 revision = "Revision: <b>%s</b><br />\n" % self.revision 112 else: 113 revision = '' 114 115 branch = "" 116 if self.branch: 117 branch = "Branch: <b>%s</b><br />\n" % self.branch 118 119 properties = [] 120 for prop in self.properties.asList(): 121 properties.append("%s: %s<br />" % (prop[0], prop[1])) 122 123 kwargs = { 'who' : html.escape(self.who), 124 'at' : self.getTime(), 125 'files' : html.UL(links) + '\n', 126 'revision' : revision, 127 'branch' : branch, 128 'comments' : html.PRE(self.comments), 129 'properties': html.UL(properties) + '\n' } 130 return html_tmpl % kwargs
131
132 - def get_HTML_box(self, url):
133 """Return the contents of a TD cell for the waterfall display. 134 135 @param url: the URL that points to an HTML page that will render 136 using our asHTML method. The Change is free to use this or ignore it 137 as it pleases. 138 139 @return: the HTML that will be put inside the table cell. Typically 140 this is just a single href named after the author of the change and 141 pointing at the passed-in 'url'. 142 """ 143 who = self.getShortAuthor() 144 if self.comments is None: 145 title = "" 146 else: 147 title = html.escape(self.comments) 148 return '<a href="%s" title="%s">%s</a>' % (url, 149 title, 150 html.escape(who))
151
152 - def getShortAuthor(self):
153 return self.who
154
155 - def getTime(self):
156 if not self.when: 157 return "?" 158 return time.strftime("%a %d %b %Y %H:%M:%S", 159 time.localtime(self.when))
160
161 - def getTimes(self):
162 return (self.when, None)
163
164 - def getText(self):
165 return [html.escape(self.who)]
166 - def getLogs(self):
167 return {}
168
169 - def getFileContents(self):
170 data = "" 171 if len(self.files) == 1: 172 if self.isdir: 173 data += "Directory: %s\n" % self.files[0] 174 else: 175 data += "File: %s\n" % self.files[0] 176 else: 177 data += "Files:\n" 178 for f in self.files: 179 data += " %s\n" % f 180 return data
181
182 - def getProperties(self):
183 data = "" 184 for prop in self.properties.asList(): 185 data += " %s: %s" % (prop[0], prop[1]) 186 return data
187
188 -class ChangeMaster(service.MultiService):
189 190 """This is the master-side service which receives file change 191 notifications from CVS. It keeps a log of these changes, enough to 192 provide for the HTML waterfall display, and to tell 193 temporarily-disconnected bots what they missed while they were 194 offline. 195 196 Change notifications come from two different kinds of sources. The first 197 is a PB service (servicename='changemaster', perspectivename='change'), 198 which provides a remote method called 'addChange', which should be 199 called with a dict that has keys 'filename' and 'comments'. 200 201 The second is a list of objects derived from the ChangeSource class. 202 These are added with .addSource(), which also sets the .changemaster 203 attribute in the source to point at the ChangeMaster. When the 204 application begins, these will be started with .start() . At shutdown 205 time, they will be terminated with .stop() . They must be persistable. 206 They are expected to call self.changemaster.addChange() with Change 207 objects. 208 209 There are several different variants of the second type of source: 210 211 - L{buildbot.changes.mail.MaildirSource} watches a maildir for CVS 212 commit mail. It uses DNotify if available, or polls every 10 213 seconds if not. It parses incoming mail to determine what files 214 were changed. 215 216 - L{buildbot.changes.freshcvs.FreshCVSSource} makes a PB 217 connection to the CVSToys 'freshcvs' daemon and relays any 218 changes it announces. 219 220 """ 221 222 implements(interfaces.IEventSource) 223 224 debug = False 225 # todo: use Maildir class to watch for changes arriving by mail 226 227 changeHorizon = 0 228
229 - def __init__(self):
230 service.MultiService.__init__(self) 231 self.changes = [] 232 # self.basedir must be filled in by the parent 233 self.nextNumber = 1
234
235 - def addSource(self, source):
236 assert interfaces.IChangeSource.providedBy(source) 237 assert service.IService.providedBy(source) 238 if self.debug: 239 print "ChangeMaster.addSource", source 240 source.setServiceParent(self)
241
242 - def removeSource(self, source):
243 assert source in self 244 if self.debug: 245 print "ChangeMaster.removeSource", source, source.parent 246 d = defer.maybeDeferred(source.disownServiceParent) 247 return d
248
249 - def addChange(self, change):
250 """Deliver a file change event. The event should be a Change object. 251 This method will timestamp the object as it is received.""" 252 log.msg("adding change, who %s, %d files, rev=%s, branch=%s, " 253 "comments %s, category %s" % (change.who, len(change.files), 254 change.revision, change.branch, 255 change.comments, change.category)) 256 change.number = self.nextNumber 257 self.nextNumber += 1 258 self.changes.append(change) 259 self.parent.addChange(change) 260 self.pruneChanges()
261
262 - def pruneChanges(self):
263 if self.changeHorizon and len(self.changes) > self.changeHorizon: 264 log.msg("pruning %i changes" % (len(self.changes) - self.changeHorizon)) 265 self.changes = self.changes[-self.changeHorizon:]
266
267 - def eventGenerator(self, branches=[], categories=[]):
268 for i in range(len(self.changes)-1, -1, -1): 269 c = self.changes[i] 270 if (not branches or c.branch in branches) and ( 271 not categories or c.category in categories): 272 yield c
273
274 - def getChangeNumbered(self, num):
275 if not self.changes: 276 return None 277 first = self.changes[0].number 278 if first + len(self.changes)-1 != self.changes[-1].number: 279 log.msg(self, 280 "lost a change somewhere: [0] is %d, [%d] is %d" % \ 281 (self.changes[0].number, 282 len(self.changes) - 1, 283 self.changes[-1].number)) 284 for c in self.changes: 285 log.msg("c[%d]: " % c.number, c) 286 return None 287 offset = num - first 288 log.msg(self, "offset", offset) 289 if 0 <= offset <= len(self.changes): 290 return self.changes[offset] 291 else: 292 return None
293
294 - def __getstate__(self):
295 d = service.MultiService.__getstate__(self) 296 del d['parent'] 297 del d['services'] # lose all children 298 del d['namedServices'] 299 return d
300
301 - def __setstate__(self, d):
302 self.__dict__ = d 303 # self.basedir must be set by the parent 304 self.services = [] # they'll be repopulated by readConfig 305 self.namedServices = {}
306 307
308 - def saveYourself(self):
309 filename = os.path.join(self.basedir, "changes.pck") 310 tmpfilename = filename + ".tmp" 311 try: 312 dump(self, open(tmpfilename, "wb")) 313 if sys.platform == 'win32': 314 # windows cannot rename a file on top of an existing one 315 if os.path.exists(filename): 316 os.unlink(filename) 317 os.rename(tmpfilename, filename) 318 except Exception, e: 319 log.msg("unable to save changes") 320 log.err()
321
322 - def stopService(self):
323 self.saveYourself() 324 return service.MultiService.stopService(self)
325
326 -class TestChangeMaster(ChangeMaster):
327 """A ChangeMaster for use in tests that does not save itself"""
328 - def stopService(self):
329 return service.MultiService.stopService(self)
330