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