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

Source Code for Module buildbot.changes.changes

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