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
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
58
59 - def __init__(self, who, files, comments, isdir=0, links=None,
60 revision=None, when=None, branch=None, category=None,
61 revlink='', properties={}):
81
87
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
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
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
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
154
156 if not self.when:
157 return "?"
158 return time.strftime("%a %d %b %Y %H:%M:%S",
159 time.localtime(self.when))
160
162 return (self.when, None)
163
165 return [html.escape(self.who)]
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
183 data = ""
184 for prop in self.properties.asList():
185 data += " %s: %s" % (prop[0], prop[1])
186 return data
187
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
226
227 changeHorizon = 0
228
230 service.MultiService.__init__(self)
231 self.changes = []
232
233 self.nextNumber = 1
234
241
248
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
266
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
293
295 d = service.MultiService.__getstate__(self)
296 del d['parent']
297 del d['services']
298 del d['namedServices']
299 return d
300
302 self.__dict__ = d
303
304 self.services = []
305 self.namedServices = {}
306
307
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
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
325
327 """A ChangeMaster for use in tests that does not save itself"""
330