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
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
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
73 self.files = files[:]
74 self.files.sort()
75
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
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
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
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
135
137 if not self.when:
138 return "?"
139 return time.strftime("%a %d %b %Y %H:%M:%S",
140 time.localtime(self.when))
141
143 return (self.when, None)
144
146 return [html.escape(self.who)]
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
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
201
202 changeHorizon = 0
203
205 service.MultiService.__init__(self)
206 self.changes = []
207
208 self.nextNumber = 1
209
216
223
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
241
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
265
267 d = service.MultiService.__getstate__(self)
268 del d['parent']
269 del d['services']
270 del d['namedServices']
271 return d
272
274 self.__dict__ = d
275
276 self.services = []
277 self.namedServices = {}
278
279
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
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
297
299 """A ChangeMaster for use in tests that does not save itself"""
302