aboutsummaryrefslogtreecommitdiffstats
path: root/BitKeeper/triggers/ciabot_bk.py
blob: d75f93257c52a03a39b5e3703e72e41aac408d6a (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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/usr/bin/env python
# ex:ts=4:sw=4:sts=4:et
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
#
# CIA bot client script for Bitkeeper repositories, written in python.
# This generates commit messages using CIA's XML commit format, and can
# deliver them using either XML-RPC or email.
#
# -- Micah Dowty <micah@navi.cx>
#
# This script is cleaner, more featureful, and faster than the shell
# script version, but won't work on systems without Python or that don't
# allow outgoing HTTP connections.
#
# To use the CIA bot in your Bitkeeper repository...
#
# 1. Customize the parameters below
#
# 2. This script should be called from your repository's post-commit
#    hook with the repository and revision as arguments. For example,
#    you could copy this script into your repository's "hooks" directory
#    and add something like the following to the "post-commit" script,
#    also in the repository's "hooks" directory:
#
#      REPOS="$1"
#      REV="$2"
#      $REPOS/hooks/ciabot_bk.py "$REPOS" "$REV" &
#
#    Or, if you have multiple project hosted, you can add each
#    project's name to the commandline in that project's post-commit
#    hook:
#
#      $REPOS/hooks/ciabot_bk.py "$REPOS" "$REV" "My Project" &
#
############# There are some parameters for this script that you can customize:

class config:
    # Replace this with your project's name, or always provide a
    # project name on the commandline.
    project = "openembedded"

    # If your repository is accessable over the web, put its base URL here
    # and 'uri' attributes will be given to all <file> elements. This means
    # that in CIA's online message viewer, each file in the tree will link
    # directly to the file in your repository
    repositoryURI = None

    # This can be the http:// URI of the CIA server to deliver commits over
    # XML-RPC, or it can be an email address to deliver using SMTP. The
    # default here should work for most people. If you need to use e-mail
    # instead, you can replace this with "cia@cia.navi.cx"
    server = "http://cia.navi.cx"

    # The SMTP server to use, only used if the CIA server above is an
    # email address
    smtpServer = "localhost"

    # The 'from' address to use. If you're delivering commits via email, set
    # this to the address you would normally send email from on this host.
    fromAddress = "cia-user@localhost"

    # When nonzero, print the message to stdout instead of delivering it to CIA
    debug = 0


############# Normally the rest of this won't need modification

import sys, os, re, urllib, xmlrpclib

class UrllibTransport(xmlrpclib.Transport):
    '''Handles an HTTP transaction to an XML-RPC server via urllib
    (urllib includes proxy-server support)
    jjk  07/02/99'''

    def __init__(self):
        self.verbose = 0

    def request(self, host, handler, request_body, verbose=0):
        '''issue XML-RPC request
        jjk  07/02/99'''
        import urllib
        urlopener = urllib.FancyURLopener()
        urlopener.addheaders = [('User-agent', self.user_agent)]
        # probably should use appropriate 'join' methods instead of 'http://'+host+handler
        f = urlopener.open('http://'+host+handler, request_body)
        return(self.parse_response(f))

class File:
    """A file in a Bitkeeper repository. According to our current
    configuration, this may have a module, branch, and URI in addition
    to a path."""

    def __init__(self, fullPath):
        self.fullPath = fullPath
        self.path = fullPath

    def getURI(self, repo):
        """Get the URI of this file, given the repository's URI. This
        encodes the full path and joins it to the given URI."""
        quotedPath = urllib.quote(self.fullPath)
        if quotedPath[0] == '/':
            quotedPath = quotedPath[1:]
        if repo[-1] != '/':
            repo = repo + '/'
        return repo + quotedPath

    def makeTag(self, config):
        """Return an XML tag for this file, using the given config"""
        attrs = {}

        if config.repositoryURI is not None:
            attrs['uri'] = self.getURI(config.repositoryURI)

        attrString = ''.join([' %s="%s"' % (key, escapeToXml(value,1))
                              for key, value in attrs.iteritems()])
        return "<file%s>%s</file>" % (attrString, escapeToXml(self.path))


class CIAClient:
    """Base CIA client class"""
    name = 'Python client for CIA'
    version = '1.0'

    def __init__(self, repository, revision, config):
        self.repository = repository
        self.revision = revision
        self.config = config

    def deliver(self, message):
        if config.debug:
            print message
        else:
            server = self.config.server
            if server.startswith('http:') or server.startswith('https:'):
                # Deliver over XML-RPC
                proxy = os.environ.get('http_proxy')
                if proxy:
                    os.environ['HTTP_PROXY'] = proxy
                    s = xmlrpclib.ServerProxy(server, UrllibTransport())
                else:
                    s = xmlrpclib.ServerProxy(server)
                s.hub.deliver(message)
            else:
                # Deliver over email
                import smtplib
                smtp = smtplib.SMTP(self.config.smtpServer)
                smtp.sendmail(self.config.fromAddress, server,
                              "From: %s\r\nTo: %s\r\n"
                              "Subject: DeliverXML\r\n\r\n%s" %
                              (self.config.fromAddress, server, message))

    def main(self):
        self.collectData()
        import socket
        try:
            self.deliver("<message>" +
                         self.makeGeneratorTag() +
                         self.makeSourceTag() +
                         self.makeBodyTag() +
                         "</message>")
            return 0
        except socket.error, e:
            print "ERROR: socket: %s" % e
            return 1

    def makeAttrTags(self, *names):
        """Given zero or more attribute names, generate XML elements for
           those attributes only if they exist and are non-None.
           """
        s = ''
        for name in names:
            if hasattr(self, name):
                v = getattr(self, name)
                if v is not None:
                    s += "<%s>%s</%s>" % (name, escapeToXml(str(v)), name)
        return s

    def makeGeneratorTag(self):
        return "<generator>%s</generator>" % self.makeAttrTags(
            'name',
            'version',
            )

    def makeSourceTag(self):
        self.project = self.config.project
        return "<source>%s</source>" % self.makeAttrTags(
            'project',
            'module',
            'branch',
            )

    def makeBodyTag(self):
        return "<body><commit>%s%s</commit></body>" % (
            self.makeAttrTags(
            'revision',
            'author',
            'log',
            'diffLines',
            ),
            self.makeFileTags(),
            )

    def makeFileTags(self):
        """Return XML tags for our file list"""
        return "<files>%s</files>" % ''.join([file.makeTag(self.config)
                                              for file in self.files])

    def collectData(self):
        raise NotImplementedError("collectData method not implemented in the base CIA client class.")

def escapeToXml(text, isAttrib=0):
    text = text.replace("&", "&amp;")
    text = text.replace("<", "&lt;")
    text = text.replace(">", "&gt;")
    if isAttrib == 1:
        text = text.replace("'", "&apos;")
        text = text.replace("\"", "&quot;")
    return text

class BKClient(CIAClient):
    """A CIA client for Bitkeeper repositories."""
    name = 'Python Bitkeeper client for CIA'
    version = '1.0'

    def __init__(self, repository, revision, config):
        CIAClient.__init__(self, repository, revision, config)
        os.chdir(self.repository)

    def bkchanges(self, command):
        """Run the given bkchanges command on our current repository and
        revision, returning all output"""
        return os.popen('bk changes %s -r"%s"' % \
                        (command, self.revision)).read()

    def collectData(self):
        self.author = self.bkchanges('-d\':P:\'').strip()
        self.log = self.bkchanges('-d\'$if(:C:){$each(:C:){:C: \\\\n}}\'').strip()
        self.diffLines = len(os.popen('bk export -tpatch -r"%s"|grep -v \'^#\'' % self.revision).read().split('\n'))
        self.files = self.collectFiles()
        self.module = os.path.basename(os.environ.get('BKD_ROOT') or '')
        self.branch = self.bkchanges('-d\':TAG:\'')

    def collectFiles(self):
        # Extract all the files from the output of 'bkchanges changed'
        lines = []
        for l in self.bkchanges('-n -v -d\'$unless(:GFILE:=ChangeSet){:GFILE:}\'').strip().split('\n'):
            if not l in lines:
                lines.append(l)
        return [ File(line) for line in lines ]


if __name__ == "__main__":
    # Print a usage message when not enough parameters are provided.
    if len(sys.argv) < 3:
        sys.stderr.write("USAGE: %s REPOS-PATH REVISION [PROJECTNAME]\n" %
                         sys.argv[0])
        sys.exit(1)

    # If a project name was provided, override the default project name.
    if len(sys.argv) > 3:
        config.project = sys.argv[3]

    # Go do the real work.
    BKClient(sys.argv[1], sys.argv[2], config).main()