Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/contrib/darcs_buildbot.py
blob: a8097d01002c0fa4ce9f47535f33b9130f93e259 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#! /usr/bin/python

# This is a script which delivers Change events from Darcs to the buildmaster
# each time a patch is pushed into a repository. Add it to the 'apply' hook
# on your canonical "central" repository, by putting something like the
# following in the _darcs/prefs/defaults file of that repository:
#
#  apply posthook /PATH/TO/darcs_buildbot.py BUILDMASTER:PORT
#  apply run-posthook
#
# (the second command is necessary to avoid the usual "do you really want to
# run this hook" prompt. Note that you cannot have multiple 'apply posthook'
# lines: if you need this, you must create a shell script to run all your
# desired commands, then point the posthook at that shell script.)
#
# Note that both Buildbot and Darcs must be installed on the repository
# machine. You will also need the Python/XML distribution installed (the
# "python2.3-xml" package under debian).

import os
import sys
import commands
import xml

from buildbot.clients import sendchange
from twisted.internet import defer, reactor
from xml.dom import minidom


def getText(node):
    return "".join([cn.data
                    for cn in node.childNodes
                    if cn.nodeType == cn.TEXT_NODE])


def getTextFromChild(parent, childtype):
    children = parent.getElementsByTagName(childtype)
    if not children:
        return ""
    return getText(children[0])


def makeChange(p):
    author = p.getAttribute("author")
    revision = p.getAttribute("hash")
    comments = (getTextFromChild(p, "name") + "\n" +
                getTextFromChild(p, "comment"))

    summary = p.getElementsByTagName("summary")[0]
    files = []
    for filenode in summary.childNodes:
        if filenode.nodeName in ("add_file", "modify_file", "remove_file"):
            filename = getText(filenode).strip()
            files.append(filename)
        elif filenode.nodeName == "move":
            from_name = filenode.getAttribute("from")
            to_name = filenode.getAttribute("to")
            files.append(to_name)

    # note that these are all unicode. Because PB can't handle unicode, we
    # encode them into ascii, which will blow up early if there's anything we
    # can't get to the far side. When we move to something that *can* handle
    # unicode (like newpb), remove this.
    author = author.encode("ascii", "replace")
    comments = comments.encode("ascii", "replace")
    files = [f.encode("ascii", "replace") for f in files]
    revision = revision.encode("ascii", "replace")

    change = {
        # note: this is more likely to be a full email address, which would
        # make the left-hand "Changes" column kind of wide. The buildmaster
        # should probably be improved to display an abbreviation of the
        # username.
        'username': author,
        'revision': revision,
        'comments': comments,
        'files': files,
        }
    return change


def getChangesFromCommand(cmd, count):
    out = commands.getoutput(cmd)
    try:
        doc = minidom.parseString(out)
    except xml.parsers.expat.ExpatError, e:
        print "failed to parse XML"
        print str(e)
        print "purported XML is:"
        print "--BEGIN--"
        print out
        print "--END--"
        sys.exit(1)

    c = doc.getElementsByTagName("changelog")[0]
    changes = []
    for i, p in enumerate(c.getElementsByTagName("patch")):
        if i >= count:
            break
        changes.append(makeChange(p))
    return changes


def getSomeChanges(count):
    cmd = "darcs changes --last=%d --xml-output --summary" % count
    return getChangesFromCommand(cmd, count)


LASTCHANGEFILE = ".darcs_buildbot-lastchange"


def findNewChanges():
    if os.path.exists(LASTCHANGEFILE):
        f = open(LASTCHANGEFILE, "r")
        lastchange = f.read()
        f.close()
    else:
        return getSomeChanges(1)
    lookback = 10
    while True:
        changes = getSomeChanges(lookback)
        # getSomeChanges returns newest-first, so changes[0] is the newest.
        # we want to scan the newest first until we find the changes we sent
        # last time, then deliver everything newer than that (and send them
        # oldest-first).
        for i, c in enumerate(changes):
            if c['revision'] == lastchange:
                newchanges = changes[:i]
                newchanges.reverse()
                return newchanges
        if 2*lookback > 100:
            raise RuntimeError("unable to find our most recent change "
                               "(%s) in the last %d changes" % (lastchange,
                                                                lookback))
        lookback = 2*lookback


def sendChanges(master):
    changes = findNewChanges()
    s = sendchange.Sender(master, None)

    d = defer.Deferred()
    reactor.callLater(0, d.callback, None)

    if not changes:
        print "darcs_buildbot.py: weird, no changes to send"
    elif len(changes) == 1:
        print "sending 1 change to buildmaster:"
    else:
        print "sending %d changes to buildmaster:" % len(changes)

    def _send(res, c):
        branch = None
        print " %s" % c['revision']
        return s.send(branch, c['revision'], c['comments'], c['files'],
                      c['username'])
    for c in changes:
        d.addCallback(_send, c)

    d.addCallbacks(s.printSuccess, s.printFailure)
    d.addBoth(s.stop)
    s.run()

    if changes:
        lastchange = changes[-1]['revision']
        f = open(LASTCHANGEFILE, "w")
        f.write(lastchange)
        f.close()


if __name__ == '__main__':
    MASTER = sys.argv[1]
    sendChanges(MASTER)