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