hook from buildbot
[moonshot.git] / admin / git_hooks / git_buildbot.py
1 #! /usr/bin/python
2
3 # This script expects one line for each new revision on the form
4 #   <oldrev> <newrev> <refname>
5 #
6 # For example:
7 #   aa453216d1b3e49e7f6f98441fa56946ddcd6a20
8 #   68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
9 #
10 # Each of these changes will be passed to the buildbot server along
11 # with any other change information we manage to extract from the
12 # repository.
13 #
14 # This script is meant to be run from hooks/post-receive in the git
15 # repository. It can also be run at client side with hooks/post-merge
16 # after using this wrapper:
17
18 #!/bin/sh
19 # PRE=$(git rev-parse 'HEAD@{1}')
20 # POST=$(git rev-parse HEAD)
21 # SYMNAME=$(git rev-parse --symbolic-full-name HEAD)
22 # echo "$PRE $POST $SYMNAME" | git_buildbot.py
23 #
24 # Largely based on contrib/hooks/post-receive-email from git.
25
26 import commands
27 import logging
28 import os
29 import re
30 import sys
31
32 from twisted.spread import pb
33 from twisted.cred import credentials
34 from twisted.internet import reactor
35
36 from buildbot.scripts import runner
37 from optparse import OptionParser
38
39 # Modify this to fit your setup, or pass in --master server:host on the
40 # command line
41
42 master = "localhost:9989"
43
44 # When sending the notification, send this category iff
45 # it's set (via --category)
46
47 category = None
48
49
50 # The GIT_DIR environment variable must have been set up so that any
51 # git commands that are executed will operate on the repository we're
52 # installed in.
53
54 changes = []
55
56
57 def connectFailed(error):
58     logging.error("Could not connect to %s: %s"
59             % (master, error.getErrorMessage()))
60     return error
61
62
63 def addChange(remote, changei):
64     logging.debug("addChange %s, %s" % (repr(remote), repr(changei)))
65     try:
66         c = changei.next()
67     except StopIteration:
68         remote.broker.transport.loseConnection()
69         return None
70
71     logging.info("New revision: %s" % c['revision'][:8])
72     for key, value in c.iteritems():
73         logging.debug("  %s: %s" % (key, value))
74
75     d = remote.callRemote('addChange', c)
76
77     # tail recursion in Twisted can blow out the stack, so we
78     # insert a callLater to delay things
79     def recurseLater(x):
80         reactor.callLater(0, addChange, remote, changei)
81     d.addCallback(recurseLater)
82     return d
83
84
85 def connected(remote):
86     return addChange(remote, changes.__iter__())
87
88
89 def grab_commit_info(c, rev):
90     # Extract information about committer and files using git show
91     f = os.popen("git show --raw --pretty=full %s" % rev, 'r')
92
93     files = []
94
95     while True:
96         line = f.readline()
97         if not line:
98             break
99
100         m = re.match(r"^:.*[MAD]\s+(.+)$", line)
101         if m:
102             logging.debug("Got file: %s" % m.group(1))
103             files.append(m.group(1))
104             continue
105
106         m = re.match(r"^Author:\s+(.+)$", line)
107         if m:
108             logging.debug("Got author: %s" % m.group(1))
109             c['who'] = m.group(1)
110
111         if re.match(r"^Merge: .*$", line):
112             files.append('merge')
113
114     c['files'] = files
115     status = f.close()
116     if status:
117         logging.warning("git show exited with status %d" % status)
118
119
120 def gen_changes(input, branch):
121     while True:
122         line = input.readline()
123         if not line:
124             break
125
126         logging.debug("Change: %s" % line)
127
128         m = re.match(r"^([0-9a-f]+) (.*)$", line.strip())
129         c = {'revision': m.group(1),
130              'comments': m.group(2),
131              'branch': branch,
132         }
133         if category:
134             c['category'] = category
135         grab_commit_info(c, m.group(1))
136         changes.append(c)
137
138
139 def gen_create_branch_changes(newrev, refname, branch):
140     # A new branch has been created. Generate changes for everything
141     # up to `newrev' which does not exist in any branch but `refname'.
142     #
143     # Note that this may be inaccurate if two new branches are created
144     # at the same time, pointing to the same commit, or if there are
145     # commits that only exists in a common subset of the new branches.
146
147     logging.info("Branch `%s' created" % branch)
148
149     f = os.popen("git rev-parse --not --branches"
150             + "| grep -v $(git rev-parse %s)" % refname
151             + "| git rev-list --reverse --pretty=oneline --stdin %s" % newrev,
152             'r')
153
154     gen_changes(f, branch)
155
156     status = f.close()
157     if status:
158         logging.warning("git rev-list exited with status %d" % status)
159
160
161 def gen_update_branch_changes(oldrev, newrev, refname, branch):
162     # A branch has been updated. If it was a fast-forward update,
163     # generate Change events for everything between oldrev and newrev.
164     #
165     # In case of a forced update, first generate a "fake" Change event
166     # rewinding the branch to the common ancestor of oldrev and
167     # newrev. Then, generate Change events for each commit between the
168     # common ancestor and newrev.
169
170     logging.info("Branch `%s' updated %s .. %s"
171             % (branch, oldrev[:8], newrev[:8]))
172
173     baserev = commands.getoutput("git merge-base %s %s" % (oldrev, newrev))
174     logging.debug("oldrev=%s newrev=%s baserev=%s" % (oldrev, newrev, baserev))
175     if baserev != oldrev:
176         c = {'revision': baserev,
177              'comments': "Rewind branch",
178              'branch': branch,
179              'who': "dummy",
180         }
181         logging.info("Branch %s was rewound to %s" % (branch, baserev[:8]))
182         files = []
183         f = os.popen("git diff --raw %s..%s" % (oldrev, baserev), 'r')
184         while True:
185             line = f.readline()
186             if not line:
187                 break
188
189             file = re.match(r"^:.*[MAD]\s*(.+)$", line).group(1)
190             logging.debug("  Rewound file: %s" % file)
191             files.append(file)
192
193         status = f.close()
194         if status:
195             logging.warning("git diff exited with status %d" % status)
196
197         if category:
198             c['category'] = category
199
200         if files:
201             c['files'] = files
202             changes.append(c)
203
204     if newrev != baserev:
205         # Not a pure rewind
206         f = os.popen("git rev-list --reverse --pretty=oneline %s..%s"
207                 % (baserev, newrev), 'r')
208         gen_changes(f, branch)
209
210         status = f.close()
211         if status:
212             logging.warning("git rev-list exited with status %d" % status)
213
214
215 def cleanup(res):
216     reactor.stop()
217
218
219 def process_changes():
220     # Read branch updates from stdin and generate Change events
221     while True:
222         line = sys.stdin.readline()
223         if not line:
224             break
225
226         [oldrev, newrev, refname] = line.split(None, 2)
227
228         # We only care about regular heads, i.e. branches
229         m = re.match(r"^refs\/heads\/(.+)$", refname)
230         if not m:
231             logging.info("Ignoring refname `%s': Not a branch" % refname)
232             continue
233
234         branch = m.group(1)
235
236         # Find out if the branch was created, deleted or updated. Branches
237         # being deleted aren't really interesting.
238         if re.match(r"^0*$", newrev):
239             logging.info("Branch `%s' deleted, ignoring" % branch)
240             continue
241         elif re.match(r"^0*$", oldrev):
242             gen_create_branch_changes(newrev, refname, branch)
243         else:
244             gen_update_branch_changes(oldrev, newrev, refname, branch)
245
246     # Submit the changes, if any
247     if not changes:
248         logging.warning("No changes found")
249         return
250
251     host, port = master.split(':')
252     port = int(port)
253
254     f = pb.PBClientFactory()
255     d = f.login(credentials.UsernamePassword("change", "changepw"))
256     reactor.connectTCP(host, port, f)
257
258     d.addErrback(connectFailed)
259     d.addCallback(connected)
260     d.addBoth(cleanup)
261
262     reactor.run()
263
264
265 def parse_options():
266     parser = OptionParser()
267     parser.add_option("-l", "--logfile", action="store", type="string",
268             help="Log to the specified file")
269     parser.add_option("-v", "--verbose", action="count",
270             help="Be more verbose. Ignored if -l is not specified.")
271     master_help = ("Build master to push to. Default is %(master)s" % 
272                    { 'master' : master })
273     parser.add_option("-m", "--master", action="store", type="string",
274             help=master_help)
275     parser.add_option("-c", "--category", action="store",
276                       type="string", help="Scheduler category to notify.")
277     options, args = parser.parse_args()
278     return options
279
280
281 # Log errors and critical messages to stderr. Optionally log
282 # information to a file as well (we'll set that up later.)
283 stderr = logging.StreamHandler(sys.stderr)
284 fmt = logging.Formatter("git_buildbot: %(levelname)s: %(message)s")
285 stderr.setLevel(logging.ERROR)
286 stderr.setFormatter(fmt)
287 logging.getLogger().addHandler(stderr)
288 logging.getLogger().setLevel(logging.DEBUG)
289
290 try:
291     options = parse_options()
292     level = logging.WARNING
293     if options.verbose:
294         level -= 10 * options.verbose
295         if level < 0:
296             level = 0
297
298     if options.logfile:
299         logfile = logging.FileHandler(options.logfile)
300         logfile.setLevel(level)
301         fmt = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
302         logfile.setFormatter(fmt)
303         logging.getLogger().addHandler(logfile)
304
305     if options.master:
306         master=options.master
307
308     if options.category:
309         category = options.category
310
311     process_changes()
312 except SystemExit:
313     pass
314 except:
315     logging.exception("Unhandled exception")
316     sys.exit(1)