Add support for logfiles to contain dicts with the follow option
[buildbot.git] / contrib / darcs_buildbot.py
blobff8bbe6acb2b14130894d8b498852a1513e7e4a8
1 #! /usr/bin/python
3 # This is a script which delivers Change events from Darcs to the buildmaster
4 # each time a patch is pushed into a repository. Add it to the 'apply' hook
5 # on your canonical "central" repository, by putting something like the
6 # following in the _darcs/prefs/defaults file of that repository:
8 # apply posthook /PATH/TO/darcs_buildbot.py BUILDMASTER:PORT
9 # apply run-posthook
11 # (the second command is necessary to avoid the usual "do you really want to
12 # run this hook" prompt. Note that you cannot have multiple 'apply posthook'
13 # lines: if you need this, you must create a shell script to run all your
14 # desired commands, then point the posthook at that shell script.)
16 # Note that both Buildbot and Darcs must be installed on the repository
17 # machine. You will also need the Python/XML distribution installed (the
18 # "python2.3-xml" package under debian).
20 import os
21 import sys
22 import commands
23 import xml
25 from buildbot.clients import sendchange
26 from twisted.internet import defer, reactor
27 from xml.dom import minidom
30 def getText(node):
31 return "".join([cn.data
32 for cn in node.childNodes
33 if cn.nodeType == cn.TEXT_NODE])
36 def getTextFromChild(parent, childtype):
37 children = parent.getElementsByTagName(childtype)
38 if not children:
39 return ""
40 return getText(children[0])
43 def makeChange(p):
44 author = p.getAttribute("author")
45 revision = p.getAttribute("hash")
46 comments = (getTextFromChild(p, "name") + "\n" +
47 getTextFromChild(p, "comment"))
49 summary = p.getElementsByTagName("summary")[0]
50 files = []
51 for filenode in summary.childNodes:
52 if filenode.nodeName in ("add_file", "modify_file", "remove_file"):
53 filename = getText(filenode).strip()
54 files.append(filename)
55 elif filenode.nodeName == "move":
56 from_name = filenode.getAttribute("from")
57 to_name = filenode.getAttribute("to")
58 files.append(to_name)
60 # note that these are all unicode. Because PB can't handle unicode, we
61 # encode them into ascii, which will blow up early if there's anything we
62 # can't get to the far side. When we move to something that *can* handle
63 # unicode (like newpb), remove this.
64 author = author.encode("ascii", "replace")
65 comments = comments.encode("ascii", "replace")
66 files = [f.encode("ascii", "replace") for f in files]
67 revision = revision.encode("ascii", "replace")
69 change = {
70 # note: this is more likely to be a full email address, which would
71 # make the left-hand "Changes" column kind of wide. The buildmaster
72 # should probably be improved to display an abbreviation of the
73 # username.
74 'username': author,
75 'revision': revision,
76 'comments': comments,
77 'files': files,
79 return change
82 def getChangesFromCommand(cmd, count):
83 out = commands.getoutput(cmd)
84 try:
85 doc = minidom.parseString(out)
86 except xml.parsers.expat.ExpatError, e:
87 print "failed to parse XML"
88 print str(e)
89 print "purported XML is:"
90 print "--BEGIN--"
91 print out
92 print "--END--"
93 sys.exit(1)
95 c = doc.getElementsByTagName("changelog")[0]
96 changes = []
97 for i, p in enumerate(c.getElementsByTagName("patch")):
98 if i >= count:
99 break
100 changes.append(makeChange(p))
101 return changes
104 def getSomeChanges(count):
105 cmd = "darcs changes --last=%d --xml-output --summary" % count
106 return getChangesFromCommand(cmd, count)
109 LASTCHANGEFILE = ".darcs_buildbot-lastchange"
112 def findNewChanges():
113 if os.path.exists(LASTCHANGEFILE):
114 f = open(LASTCHANGEFILE, "r")
115 lastchange = f.read()
116 f.close()
117 else:
118 return getSomeChanges(1)
119 lookback = 10
120 while True:
121 changes = getSomeChanges(lookback)
122 # getSomeChanges returns newest-first, so changes[0] is the newest.
123 # we want to scan the newest first until we find the changes we sent
124 # last time, then deliver everything newer than that (and send them
125 # oldest-first).
126 for i, c in enumerate(changes):
127 if c['revision'] == lastchange:
128 newchanges = changes[:i]
129 newchanges.reverse()
130 return newchanges
131 if 2*lookback > 100:
132 raise RuntimeError("unable to find our most recent change "
133 "(%s) in the last %d changes" % (lastchange,
134 lookback))
135 lookback = 2*lookback
138 def sendChanges(master):
139 changes = findNewChanges()
140 s = sendchange.Sender(master, None)
142 d = defer.Deferred()
143 reactor.callLater(0, d.callback, None)
145 if not changes:
146 print "darcs_buildbot.py: weird, no changes to send"
147 return
148 elif len(changes) == 1:
149 print "sending 1 change to buildmaster:"
150 else:
151 print "sending %d changes to buildmaster:" % len(changes)
153 # the Darcs Source class expects revision to be a context, not a
154 # hash of a patch (which is what we have in c['revision']). For
155 # the moment, we send None for everything but the most recent, because getting
156 # contexts is Hard.
158 # get the context for the most recent change
159 latestcontext = commands.getoutput("darcs changes --context")
160 changes[-1]['context'] = latestcontext
162 def _send(res, c):
163 branch = None
164 print " %s" % c['revision']
165 return s.send(branch, c.get('context'), c['comments'], c['files'],
166 c['username'])
167 for c in changes:
168 d.addCallback(_send, c)
170 d.addCallbacks(s.printSuccess, s.printFailure)
171 d.addBoth(s.stop)
172 s.run()
174 if changes:
175 lastchange = changes[-1]['revision']
176 f = open(LASTCHANGEFILE, "w")
177 f.write(lastchange)
178 f.close()
181 if __name__ == '__main__':
182 MASTER = sys.argv[1]
183 sendChanges(MASTER)