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
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).
25 from buildbot
.clients
import sendchange
26 from twisted
.internet
import defer
, reactor
27 from xml
.dom
import minidom
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
)
40 return getText(children
[0])
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]
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")
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")
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
82 def getChangesFromCommand(cmd
, count
):
83 out
= commands
.getoutput(cmd
)
85 doc
= minidom
.parseString(out
)
86 except xml
.parsers
.expat
.ExpatError
, e
:
87 print "failed to parse XML"
89 print "purported XML is:"
95 c
= doc
.getElementsByTagName("changelog")[0]
97 for i
, p
in enumerate(c
.getElementsByTagName("patch")):
100 changes
.append(makeChange(p
))
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()
118 return getSomeChanges(1)
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
126 for i
, c
in enumerate(changes
):
127 if c
['revision'] == lastchange
:
128 newchanges
= changes
[:i
]
132 raise RuntimeError("unable to find our most recent change "
133 "(%s) in the last %d changes" % (lastchange
,
135 lookback
= 2*lookback
138 def sendChanges(master
):
139 changes
= findNewChanges()
140 s
= sendchange
.Sender(master
, None)
143 reactor
.callLater(0, d
.callback
, None)
146 print "darcs_buildbot.py: weird, no changes to send"
148 elif len(changes
) == 1:
149 print "sending 1 change to buildmaster:"
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
158 # get the context for the most recent change
159 latestcontext
= commands
.getoutput("darcs changes --context")
160 changes
[-1]['context'] = latestcontext
164 print " %s" % c
['revision']
165 return s
.send(branch
, c
.get('context'), c
['comments'], c
['files'],
168 d
.addCallback(_send
, c
)
170 d
.addCallbacks(s
.printSuccess
, s
.printFailure
)
175 lastchange
= changes
[-1]['revision']
176 f
= open(LASTCHANGEFILE
, "w")
181 if __name__
== '__main__':