3 # This script expects one line for each new revision on the form
4 # <oldrev> <newrev> <refname>
7 # aa453216d1b3e49e7f6f98441fa56946ddcd6a20
8 # 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
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:
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
24 # Largely based on contrib/hooks/post-receive-email from git.
33 from twisted.spread import pb
34 from twisted.cred import credentials
35 from twisted.internet import reactor
37 from buildbot.scripts import runner
38 from optparse import OptionParser
40 # Modify this to fit your setup, or pass in --master server:host on the
43 master = "localhost:9989"
45 # When sending the notification, send this category iff
46 # it's set (via --category)
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
58 def connectFailed(error):
59 logging.error("Could not connect to %s: %s"
60 % (master, error.getErrorMessage()))
64 def addChange(remote, changei):
65 logging.debug("addChange %s, %s" % (repr(remote), repr(changei)))
69 remote.broker.transport.loseConnection()
72 logging.info("New revision: %s" % c['revision'][:8])
73 for key, value in c.iteritems():
74 logging.debug(" %s: %s" % (key, value))
76 d = remote.callRemote('addChange', c)
78 # tail recursion in Twisted can blow out the stack, so we
79 # insert a callLater to delay things
81 reactor.callLater(0, addChange, remote, changei)
82 d.addCallback(recurseLater)
86 def connected(remote):
87 return addChange(remote, changes.__iter__())
89 def add_package(c, files, rev):
90 packages_str = commands.getoutput("git show %s:source_packages" % rev)
91 packages = packages_str.split("\n")
93 class NoPackage(exceptions.Exception): pass
99 packages_found[p] = True
102 if not found: raise NoPackage()
106 "package": " ".join(packages_found.keys())
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')
121 m = re.match(r"^:.*[MAD]\s+(.+)$", line)
123 logging.debug("Got file: %s" % m.group(1))
124 files.append(m.group(1))
127 m = re.match(r"^Author:\s+(.+)$", line)
129 logging.debug("Got author: %s" % m.group(1))
130 c['who'] = m.group(1)
132 if re.match(r"^Merge: .*$", line):
133 files.append('merge')
138 logging.warning("git show exited with status %d" % status)
139 add_package(c, files, rev)
141 def gen_changes(input, branch):
143 line = input.readline()
147 logging.debug("Change: %s" % line)
149 m = re.match(r"^([0-9a-f]+) (.*)$", line.strip())
150 c = {'revision': m.group(1),
151 'comments': m.group(2),
155 c['category'] = category
156 grab_commit_info(c, m.group(1))
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'.
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.
168 logging.info("Branch `%s' created" % branch)
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,
175 gen_changes(f, branch)
179 logging.warning("git rev-list exited with status %d" % status)
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.
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.
191 logging.info("Branch `%s' updated %s .. %s"
192 % (branch, oldrev[:8], newrev[:8]))
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",
202 logging.info("Branch %s was rewound to %s" % (branch, baserev[:8]))
204 f = os.popen("git diff --raw %s..%s" % (oldrev, baserev), 'r')
210 file = re.match(r"^:.*[MAD]\s*(.+)$", line).group(1)
211 logging.debug(" Rewound file: %s" % file)
216 logging.warning("git diff exited with status %d" % status)
219 c['category'] = category
225 if newrev != baserev:
227 f = os.popen("git rev-list --reverse --pretty=oneline %s..%s"
228 % (baserev, newrev), 'r')
229 gen_changes(f, branch)
233 logging.warning("git rev-list exited with status %d" % status)
240 def process_changes():
241 # Read branch updates from stdin and generate Change events
243 line = sys.stdin.readline()
247 [oldrev, newrev, refname] = line.split(None, 2)
249 # We only care about regular heads, i.e. branches
250 m = re.match(r"^refs\/heads\/(.+)$", refname)
252 logging.info("Ignoring refname `%s': Not a branch" % refname)
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)
262 elif re.match(r"^0*$", oldrev):
263 gen_create_branch_changes(newrev, refname, branch)
265 gen_update_branch_changes(oldrev, newrev, refname, branch)
267 # Submit the changes, if any
269 logging.warning("No changes found")
272 host, port = master.split(':')
275 f = pb.PBClientFactory()
276 d = f.login(credentials.UsernamePassword("change", "changepw"))
277 reactor.connectTCP(host, port, f)
279 d.addErrback(connectFailed)
280 d.addCallback(connected)
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",
296 parser.add_option("-c", "--category", action="store",
297 type="string", help="Scheduler category to notify.")
298 options, args = parser.parse_args()
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)
312 options = parse_options()
313 level = logging.WARNING
315 level -= 10 * options.verbose
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)
327 master=options.master
330 category = options.category
336 logging.exception("Unhandled exception")