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