editing improvement to docstring
[buildbot.git] / contrib / git_buildbot.py
blob036110bce2d200cf219d5badbe270ef4fb8ebb69
1 #! /usr/bin/env python
3 # This script is meant to run from hooks/post-receive in the git
4 # repository. It expects one line for each new revision on the form
5 # <oldrev> <newrev> <refname>
7 # For example:
8 # aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
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.
14 # Largely based on contrib/hooks/post-receive-email from git.
16 import commands, logging, os, re, sys
18 from twisted.spread import pb
19 from twisted.cred import credentials
20 from twisted.internet import reactor
22 from buildbot.scripts import runner
23 from optparse import OptionParser
25 # Modify this to fit your setup
27 master = "localhost:9989"
29 # The GIT_DIR environment variable must have been set up so that any
30 # git commands that are executed will operate on the repository we're
31 # installed in.
33 changes = []
35 def connectFailed(error):
36 logging.error("Could not connect to %s: %s"
37 % (master, error.getErrorMessage()))
38 return error
40 def addChange(dummy, remote, changei):
41 logging.debug("addChange %s, %s" % (repr(remote), repr(changei)))
42 try:
43 c = changei.next()
44 except StopIteration:
45 remote.broker.transport.loseConnection()
46 return None
48 logging.info("New revision: %s" % c['revision'][:8])
49 for key, value in c.iteritems():
50 logging.debug(" %s: %s" % (key, value))
52 d = remote.callRemote('addChange', c)
53 d.addCallback(addChange, remote, changei)
54 return d
56 def connected(remote):
57 return addChange(None, remote, changes.__iter__())
59 def grab_commit_info(c, rev):
60 # Extract information about committer and files using git-show
61 f = os.popen("git-show --raw --pretty=full %s" % rev, 'r')
63 files = []
65 while True:
66 line = f.readline()
67 if not line:
68 break
70 m = re.match(r"^:.*[MAD]\s+(.+)$", line)
71 if m:
72 logging.debug("Got file: %s" % m.group(1))
73 files.append(m.group(1))
74 continue
76 m = re.match(r"^Commit:\s+(.+)$", line)
77 if m:
78 logging.debug("Got committer: %s" % m.group(1))
79 c['who'] = m.group(1)
81 c['files'] = files
82 status = f.close()
83 if status:
84 logging.warning("git-show exited with status %d" % status)
86 def gen_changes(input, branch):
87 while True:
88 line = input.readline()
89 if not line:
90 break
92 logging.debug("Change: %s" % line)
94 m = re.match(r"^([0-9a-f]+) (.*)$", line.strip())
95 c = { 'revision': m.group(1), 'comments': m.group(2),
96 'branch': branch }
97 grab_commit_info(c, m.group(1))
98 changes.append(c)
100 def gen_create_branch_changes(newrev, refname, branch):
101 # A new branch has been created. Generate changes for everything
102 # up to `newrev' which does not exist in any branch but `refname'.
104 # Note that this may be inaccurate if two new branches are created
105 # at the same time, pointing to the same commit, or if there are
106 # commits that only exists in a common subset of the new branches.
108 logging.info("Branch `%s' created" % branch)
110 f = os.popen("git-rev-parse --not --branches"
111 + "| grep -v $(git-rev-parse %s)" % refname
112 + "| git-rev-list --reverse --pretty=oneline --stdin %s" % newrev,
113 'r')
115 gen_changes(f, branch)
117 status = f.close()
118 if status:
119 logging.warning("git-rev-list exited with status %d" % status)
121 def gen_update_branch_changes(oldrev, newrev, refname, branch):
122 # A branch has been updated. If it was a fast-forward update,
123 # generate Change events for everything between oldrev and newrev.
125 # In case of a forced update, first generate a "fake" Change event
126 # rewinding the branch to the common ancestor of oldrev and
127 # newrev. Then, generate Change events for each commit between the
128 # common ancestor and newrev.
130 logging.info("Branch `%s' updated %s .. %s"
131 % (branch, oldrev[:8], newrev[:8]))
133 baserev = commands.getoutput("git-merge-base %s %s" % (oldrev, newrev))
134 logging.debug("oldrev=%s newrev=%s baserev=%s" % (oldrev, newrev, baserev))
135 if baserev != oldrev:
136 c = { 'revision': baserev, 'comments': "Rewind branch",
137 'branch': branch, 'who': "dummy" }
139 logging.info("Branch %s was rewound to %s" % (branch, baserev[:8]))
140 files = []
141 f = os.popen("git-diff --raw %s..%s" % (oldrev, baserev), 'r')
142 while True:
143 line = f.readline()
144 if not line:
145 break
147 file = re.match(r"^:.*[MAD]\s*(.+)$", line).group(1)
148 logging.debug(" Rewound file: %s" % file)
149 files.append(file)
151 status = f.close()
152 if status:
153 logging.warning("git-diff exited with status %d" % status)
155 if files:
156 c['files'] = files
157 changes.append(c)
159 if newrev != baserev:
160 # Not a pure rewind
161 f = os.popen("git-rev-list --reverse --pretty=oneline %s..%s"
162 % (baserev, newrev), 'r')
163 gen_changes(f, branch)
165 status = f.close()
166 if status:
167 logging.warning("git-rev-list exited with status %d" % status)
169 def cleanup(res):
170 reactor.stop()
172 def process_changes():
173 # Read branch updates from stdin and generate Change events
174 while True:
175 line = sys.stdin.readline()
176 if not line:
177 break
179 [oldrev, newrev, refname] = line.split(None, 2)
181 # We only care about regular heads, i.e. branches
182 m = re.match(r"^refs\/heads\/(.+)$", refname)
183 if not m:
184 logging.info("Ignoring refname `%s': Not a branch" % refname)
185 continue
187 branch = m.group(1)
189 # Find out if the branch was created, deleted or updated. Branches
190 # being deleted aren't really interesting.
191 if re.match(r"^0*$", newrev):
192 logging.info("Branch `%s' deleted, ignoring" % branch)
193 continue
194 elif re.match(r"^0*$", oldrev):
195 gen_create_branch_changes(newrev, refname, branch)
196 else:
197 gen_update_branch_changes(oldrev, newrev, refname, branch)
199 # Submit the changes, if any
200 if not changes:
201 logging.warning("No changes found")
202 return
204 host, port = master.split(':')
205 port = int(port)
207 f = pb.PBClientFactory()
208 d = f.login(credentials.UsernamePassword("change", "changepw"))
209 reactor.connectTCP(host, port, f)
211 d.addErrback(connectFailed)
212 d.addCallback(connected)
213 d.addBoth(cleanup)
215 reactor.run()
217 def parse_options():
218 parser = OptionParser()
219 parser.add_option("-l", "--logfile", action="store", type="string",
220 help="Log to the specified file")
221 parser.add_option("-v", "--verbose", action="count",
222 help="Be more verbose. Ignored if -l is not specified.")
223 options, args = parser.parse_args()
224 return options
226 # Log errors and critical messages to stderr. Optionally log
227 # information to a file as well (we'll set that up later.)
228 stderr = logging.StreamHandler(sys.stderr)
229 fmt = logging.Formatter("git_buildbot: %(levelname)s: %(message)s")
230 stderr.setLevel(logging.ERROR)
231 stderr.setFormatter(fmt)
232 logging.getLogger().addHandler(stderr)
233 logging.getLogger().setLevel(logging.DEBUG)
235 try:
236 options = parse_options()
237 level = logging.WARNING
238 if options.verbose:
239 level -= 10 * options.verbose
240 if level < 0:
241 level = 0
243 if options.logfile:
244 logfile = logging.FileHandler(options.logfile)
245 logfile.setLevel(level)
246 fmt = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
247 logfile.setFormatter(fmt)
248 logging.getLogger().addHandler(logfile)
250 process_changes()
251 except:
252 logging.exception("Unhandled exception")
253 sys.exit(1)