more NEWS items
[buildbot.git] / buildbot / changes / changes.py
blob4757253fa9b8fd3f84f896adca868f0292ae67b6
1 #! /usr/bin/python
3 from __future__ import generators
4 import sys, os, time
5 try:
6 import cPickle
7 pickle = cPickle
8 except ImportError:
9 import pickle
11 from twisted.python import log
12 from twisted.internet import defer
13 from twisted.application import service
14 from twisted.web import html
16 from buildbot import interfaces, util
17 from buildbot.twcompat import implements, providedBy
19 html_tmpl = """
20 <p>Changed by: <b>%(who)s</b><br />
21 Changed at: <b>%(at)s</b><br />
22 %(branch)s
23 %(revision)s
24 <br />
26 Changed files:
27 %(files)s
29 Comments:
30 %(comments)s
31 </p>
32 """
34 class Change:
35 """I represent a single change to the source tree. This may involve
36 several files, but they are all changed by the same person, and there is
37 a change comment for the group as a whole.
39 If the version control system supports sequential repository- (or
40 branch-) wide change numbers (like SVN, P4, and Arch), then revision=
41 should be set to that number. The highest such number will be used at
42 checkout time to get the correct set of files.
44 If it does not (like CVS), when= should be set to the timestamp (seconds
45 since epoch, as returned by time.time()) when the change was made. when=
46 will be filled in for you (to the current time) if you omit it, which is
47 suitable for ChangeSources which have no way of getting more accurate
48 timestamps.
50 Changes should be submitted to ChangeMaster.addChange() in
51 chronologically increasing order. Out-of-order changes will probably
52 cause the html.Waterfall display to be corrupted."""
54 if implements:
55 implements(interfaces.IStatusEvent)
56 else:
57 __implements__ = interfaces.IStatusEvent,
59 number = None
61 links = []
62 branch = None
63 revision = None # used to create a source-stamp
65 def __init__(self, who, files, comments, isdir=0, links=[],
66 revision=None, when=None, branch=None):
67 self.who = who
68 self.files = files
69 self.comments = comments
70 self.isdir = isdir
71 self.links = links
72 self.revision = revision
73 if when is None:
74 when = util.now()
75 self.when = when
76 self.branch = branch
78 def asText(self):
79 data = ""
80 data += self.getFileContents()
81 data += "At: %s\n" % self.getTime()
82 data += "Changed By: %s\n" % self.who
83 data += "Comments: %s\n\n" % self.comments
84 return data
86 def asHTML(self):
87 links = []
88 for file in self.files:
89 link = filter(lambda s: s.find(file) != -1, self.links)
90 if len(link) == 1:
91 # could get confused
92 links.append('<a href="%s"><b>%s</b></a>' % (link[0], file))
93 else:
94 links.append('<b>%s</b>' % file)
95 revision = ""
96 if self.revision:
97 revision = "Revision: <b>%s</b><br />\n" % self.revision
98 branch = ""
99 if self.branch:
100 branch = "Branch: <b>%s</b><br />\n" % self.branch
102 kwargs = { 'who' : html.escape(self.who),
103 'at' : self.getTime(),
104 'files' : html.UL(links) + '\n',
105 'revision': revision,
106 'branch' : branch,
107 'comments': html.PRE(self.comments) }
108 return html_tmpl % kwargs
110 def getTime(self):
111 if not self.when:
112 return "?"
113 return time.strftime("%a %d %b %Y %H:%M:%S",
114 time.localtime(self.when))
116 def getTimes(self):
117 return (self.when, None)
119 def getText(self):
120 return [html.escape(self.who)]
121 def getColor(self):
122 return "white"
123 def getLogs(self):
124 return {}
126 def getFileContents(self):
127 data = ""
128 if len(self.files) == 1:
129 if self.isdir:
130 data += "Directory: %s\n" % self.files[0]
131 else:
132 data += "File: %s\n" % self.files[0]
133 else:
134 data += "Files:\n"
135 for f in self.files:
136 data += " %s\n" % f
137 return data
139 class ChangeMaster(service.MultiService):
141 """This is the master-side service which receives file change
142 notifications from CVS. It keeps a log of these changes, enough to
143 provide for the HTML waterfall display, and to tell
144 temporarily-disconnected bots what they missed while they were
145 offline.
147 Change notifications come from two different kinds of sources. The first
148 is a PB service (servicename='changemaster', perspectivename='change'),
149 which provides a remote method called 'addChange', which should be
150 called with a dict that has keys 'filename' and 'comments'.
152 The second is a list of objects derived from the ChangeSource class.
153 These are added with .addSource(), which also sets the .changemaster
154 attribute in the source to point at the ChangeMaster. When the
155 application begins, these will be started with .start() . At shutdown
156 time, they will be terminated with .stop() . They must be persistable.
157 They are expected to call self.changemaster.addChange() with Change
158 objects.
160 There are several different variants of the second type of source:
162 - L{buildbot.changes.mail.MaildirSource} watches a maildir for CVS
163 commit mail. It uses DNotify if available, or polls every 10
164 seconds if not. It parses incoming mail to determine what files
165 were changed.
167 - L{buildbot.changes.freshcvs.FreshCVSSource} makes a PB
168 connection to the CVSToys 'freshcvs' daemon and relays any
169 changes it announces.
173 debug = False
174 # todo: use Maildir class to watch for changes arriving by mail
176 def __init__(self):
177 service.MultiService.__init__(self)
178 self.changes = []
179 # self.basedir must be filled in by the parent
180 self.nextNumber = 1
182 def addSource(self, source):
183 assert providedBy(source, interfaces.IChangeSource)
184 assert providedBy(source, service.IService)
185 if self.debug:
186 print "ChangeMaster.addSource", source
187 source.setServiceParent(self)
189 def removeSource(self, source):
190 assert source in self
191 if self.debug:
192 print "ChangeMaster.removeSource", source, source.parent
193 d = defer.maybeDeferred(source.disownServiceParent)
194 return d
196 def addChange(self, change):
197 """Deliver a file change event. The event should be a Change object.
198 This method will timestamp the object as it is received."""
199 log.msg("adding change, who %s, %d files, rev=%s, branch=%s, "
200 "comments %s" % (change.who, len(change.files),
201 change.revision, change.branch,
202 change.comments))
203 change.number = self.nextNumber
204 self.nextNumber += 1
205 self.changes.append(change)
206 self.parent.addChange(change)
207 # TODO: call pruneChanges after a while
209 def pruneChanges(self):
210 self.changes = self.changes[-100:] # or something
212 def eventGenerator(self):
213 for i in range(len(self.changes)-1, -1, -1):
214 c = self.changes[i]
215 yield c
217 def getChangeNumbered(self, num):
218 if not self.changes:
219 return None
220 first = self.changes[0].number
221 if first + len(self.changes)-1 != self.changes[-1].number:
222 log.msg(self,
223 "lost a change somewhere: [0] is %d, [%d] is %d" % \
224 (self.changes[0].number,
225 len(self.changes) - 1,
226 self.changes[-1].number))
227 for c in self.changes:
228 log.msg("c[%d]: " % c.number, c)
229 return None
230 offset = num - first
231 log.msg(self, "offset", offset)
232 return self.changes[offset]
234 def __getstate__(self):
235 d = service.MultiService.__getstate__(self)
236 del d['parent']
237 del d['services'] # lose all children
238 del d['namedServices']
239 return d
241 def __setstate__(self, d):
242 self.__dict__ = d
243 # self.basedir must be set by the parent
244 self.services = [] # they'll be repopulated by readConfig
245 self.namedServices = {}
248 def saveYourself(self):
249 filename = os.path.join(self.basedir, "changes.pck")
250 tmpfilename = filename + ".tmp"
251 try:
252 pickle.dump(self, open(tmpfilename, "wb"))
253 if sys.platform == 'win32':
254 # windows cannot rename a file on top of an existing one
255 if os.path.exists(filename):
256 os.unlink(filename)
257 os.rename(tmpfilename, filename)
258 except Exception, e:
259 log.msg("unable to save changes")
260 log.err()
262 def stopService(self):
263 self.saveYourself()
264 return service.MultiService.stopService(self)