NEWS: fix minor typo
[buildbot.git] / contrib / darcs_buildbot.py
blobdfd6deda0121aa39029066a8d75686580c1a7fc7
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, sys, commands
21 from buildbot.clients import sendchange
22 from twisted.internet import defer, reactor
23 import xml
24 from xml.dom import minidom
26 def getText(node):
27 return "".join([cn.data
28 for cn in node.childNodes
29 if cn.nodeType == cn.TEXT_NODE])
30 def getTextFromChild(parent, childtype):
31 children = parent.getElementsByTagName(childtype)
32 if not children:
33 return ""
34 return getText(children[0])
36 def makeChange(p):
38 author = p.getAttribute("author")
39 revision = p.getAttribute("hash")
40 comments = (getTextFromChild(p, "name") + "\n" +
41 getTextFromChild(p, "comment"))
43 summary = p.getElementsByTagName("summary")[0]
44 files = []
45 for filenode in summary.childNodes:
46 if filenode.nodeName in ("add_file", "modify_file", "remove_file"):
47 filename = getText(filenode).strip()
48 files.append(filename)
49 elif filenode.nodeName == "move":
50 from_name = filenode.getAttribute("from")
51 to_name = filenode.getAttribute("to")
52 files.append(to_name)
54 # note that these are all unicode. Because PB can't handle unicode, we
55 # encode them into ascii, which will blow up early if there's anything we
56 # can't get to the far side. When we move to something that *can* handle
57 # unicode (like newpb), remove this.
58 author = author.encode("ascii")
59 comments = comments.encode("ascii")
60 files = [f.encode("ascii") for f in files]
61 revision = revision.encode("ascii")
63 change = {
64 # note: this is more likely to be a full email address, which would
65 # make the left-hand "Changes" column kind of wide. The buildmaster
66 # should probably be improved to display an abbreviation of the
67 # username.
68 'username': author,
69 'revision': revision,
70 'comments': comments,
71 'files': files,
73 return change
77 def getChangesFromCommand(cmd, count):
78 out = commands.getoutput(cmd)
79 try:
80 doc = minidom.parseString(out)
81 except xml.parsers.expat.ExpatError, e:
82 print "failed to parse XML"
83 print str(e)
84 print "purported XML is:"
85 print "--BEGIN--"
86 print out
87 print "--END--"
88 sys.exit(1)
90 c = doc.getElementsByTagName("changelog")[0]
91 changes = []
92 for i,p in enumerate(c.getElementsByTagName("patch")):
93 if i >= count:
94 break
95 changes.append(makeChange(p))
96 return changes
98 def getSomeChanges(count):
99 cmd = "darcs changes --last=%d --xml-output --summary" % count
100 return getChangesFromCommand(cmd, count)
103 LASTCHANGEFILE = ".darcs_buildbot-lastchange"
105 def findNewChanges():
106 if os.path.exists(LASTCHANGEFILE):
107 f = open(LASTCHANGEFILE, "r")
108 lastchange = f.read()
109 f.close()
110 else:
111 return getSomeChanges(1)
112 lookback = 10
113 while True:
114 changes = getSomeChanges(lookback)
115 # getSomeChanges returns newest-first, so changes[0] is the newest.
116 # we want to scan the newest first until we find the changes we sent
117 # last time, then deliver everything newer than that (and send them
118 # oldest-first).
119 for i,c in enumerate(changes):
120 if c['revision'] == lastchange:
121 newchanges = changes[:i]
122 newchanges.reverse()
123 return newchanges
124 if 2*lookback > 100:
125 raise RuntimeError("unable to find our most recent change "
126 "(%s) in the last %d changes" % (lastchange,
127 lookback))
128 lookback = 2*lookback
130 def sendChanges(master):
131 changes = findNewChanges()
132 s = sendchange.Sender(master, None)
134 d = defer.Deferred()
135 reactor.callLater(0, d.callback, None)
137 if not changes:
138 print "darcs_buildbot.py: weird, no changes to send"
139 elif len(changes) == 1:
140 print "sending 1 change to buildmaster:"
141 else:
142 print "sending %d changes to buildmaster:" % len(changes)
144 def _send(res, c):
145 branch = None
146 print " %s" % c['revision']
147 return s.send(branch, c['revision'], c['comments'], c['files'],
148 c['username'])
149 for c in changes:
150 d.addCallback(_send, c)
152 d.addCallbacks(s.printSuccess, s.printFailure)
153 d.addBoth(s.stop)
154 s.run()
156 if changes:
157 lastchange = changes[-1]['revision']
158 f = open(LASTCHANGEFILE, "w")
159 f.write(lastchange)
160 f.close()
162 if __name__ == '__main__':
163 MASTER = sys.argv[1]
164 sendChanges(MASTER)