(refs #35) fix dependent scheduler re-checking: make calculation of upstream lazier
[buildbot.git] / buildbot / changes / changes.py
blob38927c0856ff1a03c0d5d8fc5b661d56af8c0a16
2 import sys, os, time
3 from cPickle import dump
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
11 from buildbot import interfaces, util
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 />
20 Changed files:
21 %(files)s
23 Comments:
24 %(comments)s
25 </p>
26 """
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.
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.
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.
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."""
48 implements(interfaces.IStatusEvent)
50 number = None
52 links = []
53 branch = None
54 revision = None # used to create a source-stamp
56 def __init__(self, who, files, comments, isdir=0, links=[],
57 revision=None, when=None, branch=None, category=None,
58 revlink=''):
59 self.who = who
60 self.comments = comments
61 self.isdir = isdir
62 self.links = links
63 self.revision = revision
64 if when is None:
65 when = util.now()
66 self.when = when
67 self.branch = branch
68 self.category = category
69 self.revlink = revlink
71 # keep a sorted list of the files, for easier display
72 self.files = files[:]
73 self.files.sort()
75 def asText(self):
76 data = ""
77 data += self.getFileContents()
78 data += "At: %s\n" % self.getTime()
79 data += "Changed By: %s\n" % self.who
80 data += "Comments: %s\n\n" % self.comments
81 return data
83 def asHTML(self):
84 links = []
85 for file in self.files:
86 link = filter(lambda s: s.find(file) != -1, self.links)
87 if len(link) == 1:
88 # could get confused
89 links.append('<a href="%s"><b>%s</b></a>' % (link[0], file))
90 else:
91 links.append('<b>%s</b>' % file)
92 revlink = ""
93 if self.revision:
94 if getattr(self, 'revlink', ""):
95 revision = 'Revision: <a href="%s"><b>%s</b></a>\n' % (
96 self.revlink, self.revision)
97 else:
98 revision = "Revision: <b>%s</b><br />\n" % self.revision
100 branch = ""
101 if self.branch:
102 branch = "Branch: <b>%s</b><br />\n" % self.branch
104 kwargs = { 'who' : html.escape(self.who),
105 'at' : self.getTime(),
106 'files' : html.UL(links) + '\n',
107 'revision': revision,
108 'branch' : branch,
109 'comments': html.PRE(self.comments) }
110 return html_tmpl % kwargs
112 def get_HTML_box(self, url):
113 """Return the contents of a TD cell for the waterfall display.
115 @param url: the URL that points to an HTML page that will render
116 using our asHTML method. The Change is free to use this or ignore it
117 as it pleases.
119 @return: the HTML that will be put inside the table cell. Typically
120 this is just a single href named after the author of the change and
121 pointing at the passed-in 'url'.
123 who = self.getShortAuthor()
124 if self.comments is None:
125 title = ""
126 else:
127 title = html.escape(self.comments)
128 return '<a href="%s" title="%s">%s</a>' % (url,
129 title,
130 html.escape(who))
132 def getShortAuthor(self):
133 return self.who
135 def getTime(self):
136 if not self.when:
137 return "?"
138 return time.strftime("%a %d %b %Y %H:%M:%S",
139 time.localtime(self.when))
141 def getTimes(self):
142 return (self.when, None)
144 def getText(self):
145 return [html.escape(self.who)]
146 def getLogs(self):
147 return {}
149 def getFileContents(self):
150 data = ""
151 if len(self.files) == 1:
152 if self.isdir:
153 data += "Directory: %s\n" % self.files[0]
154 else:
155 data += "File: %s\n" % self.files[0]
156 else:
157 data += "Files:\n"
158 for f in self.files:
159 data += " %s\n" % f
160 return data
162 class ChangeMaster(service.MultiService):
164 """This is the master-side service which receives file change
165 notifications from CVS. It keeps a log of these changes, enough to
166 provide for the HTML waterfall display, and to tell
167 temporarily-disconnected bots what they missed while they were
168 offline.
170 Change notifications come from two different kinds of sources. The first
171 is a PB service (servicename='changemaster', perspectivename='change'),
172 which provides a remote method called 'addChange', which should be
173 called with a dict that has keys 'filename' and 'comments'.
175 The second is a list of objects derived from the ChangeSource class.
176 These are added with .addSource(), which also sets the .changemaster
177 attribute in the source to point at the ChangeMaster. When the
178 application begins, these will be started with .start() . At shutdown
179 time, they will be terminated with .stop() . They must be persistable.
180 They are expected to call self.changemaster.addChange() with Change
181 objects.
183 There are several different variants of the second type of source:
185 - L{buildbot.changes.mail.MaildirSource} watches a maildir for CVS
186 commit mail. It uses DNotify if available, or polls every 10
187 seconds if not. It parses incoming mail to determine what files
188 were changed.
190 - L{buildbot.changes.freshcvs.FreshCVSSource} makes a PB
191 connection to the CVSToys 'freshcvs' daemon and relays any
192 changes it announces.
196 implements(interfaces.IEventSource)
198 debug = False
199 # todo: use Maildir class to watch for changes arriving by mail
201 changeHorizon = 0
203 def __init__(self):
204 service.MultiService.__init__(self)
205 self.changes = []
206 # self.basedir must be filled in by the parent
207 self.nextNumber = 1
209 def addSource(self, source):
210 assert interfaces.IChangeSource.providedBy(source)
211 assert service.IService.providedBy(source)
212 if self.debug:
213 print "ChangeMaster.addSource", source
214 source.setServiceParent(self)
216 def removeSource(self, source):
217 assert source in self
218 if self.debug:
219 print "ChangeMaster.removeSource", source, source.parent
220 d = defer.maybeDeferred(source.disownServiceParent)
221 return d
223 def addChange(self, change):
224 """Deliver a file change event. The event should be a Change object.
225 This method will timestamp the object as it is received."""
226 log.msg("adding change, who %s, %d files, rev=%s, branch=%s, "
227 "comments %s, category %s" % (change.who, len(change.files),
228 change.revision, change.branch,
229 change.comments, change.category))
230 change.number = self.nextNumber
231 self.nextNumber += 1
232 self.changes.append(change)
233 self.parent.addChange(change)
234 self.pruneChanges()
236 def pruneChanges(self):
237 if self.changeHorizon and len(self.changes) > self.changeHorizon:
238 log.msg("pruning %i changes" % (len(self.changes) - self.changeHorizon))
239 self.changes = self.changes[-self.changeHorizon:]
241 def eventGenerator(self, branches=[]):
242 for i in range(len(self.changes)-1, -1, -1):
243 c = self.changes[i]
244 if not branches or c.branch in branches:
245 yield c
247 def getChangeNumbered(self, num):
248 if not self.changes:
249 return None
250 first = self.changes[0].number
251 if first + len(self.changes)-1 != self.changes[-1].number:
252 log.msg(self,
253 "lost a change somewhere: [0] is %d, [%d] is %d" % \
254 (self.changes[0].number,
255 len(self.changes) - 1,
256 self.changes[-1].number))
257 for c in self.changes:
258 log.msg("c[%d]: " % c.number, c)
259 return None
260 offset = num - first
261 log.msg(self, "offset", offset)
262 return self.changes[offset]
264 def __getstate__(self):
265 d = service.MultiService.__getstate__(self)
266 del d['parent']
267 del d['services'] # lose all children
268 del d['namedServices']
269 return d
271 def __setstate__(self, d):
272 self.__dict__ = d
273 # self.basedir must be set by the parent
274 self.services = [] # they'll be repopulated by readConfig
275 self.namedServices = {}
278 def saveYourself(self):
279 filename = os.path.join(self.basedir, "changes.pck")
280 tmpfilename = filename + ".tmp"
281 try:
282 dump(self, open(tmpfilename, "wb"))
283 if sys.platform == 'win32':
284 # windows cannot rename a file on top of an existing one
285 if os.path.exists(filename):
286 os.unlink(filename)
287 os.rename(tmpfilename, filename)
288 except Exception, e:
289 log.msg("unable to save changes")
290 log.err()
292 def stopService(self):
293 self.saveYourself()
294 return service.MultiService.stopService(self)
296 class TestChangeMaster(ChangeMaster):
297 """A ChangeMaster for use in tests that does not save itself"""
298 def stopService(self):
299 return service.MultiService.stopService(self)