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.
32 from twisted
.spread
import pb
33 from twisted
.cred
import credentials
34 from twisted
.internet
import reactor
36 from buildbot
.scripts
import runner
37 from optparse
import OptionParser
39 # Modify this to fit your setup, or pass in --master server:host on the
42 master
= "localhost:9989"
44 # The GIT_DIR environment variable must have been set up so that any
45 # git commands that are executed will operate on the repository we're
51 def connectFailed(error
):
52 logging
.error("Could not connect to %s: %s"
53 % (master
, error
.getErrorMessage()))
57 def addChange(dummy
, remote
, changei
):
58 logging
.debug("addChange %s, %s" % (repr(remote
), repr(changei
)))
62 remote
.broker
.transport
.loseConnection()
65 logging
.info("New revision: %s" % c
['revision'][:8])
66 for key
, value
in c
.iteritems():
67 logging
.debug(" %s: %s" % (key
, value
))
69 d
= remote
.callRemote('addChange', c
)
70 d
.addCallback(addChange
, remote
, changei
)
74 def connected(remote
):
75 return addChange(None, remote
, changes
.__iter
__())
78 def grab_commit_info(c
, rev
):
79 # Extract information about committer and files using git show
80 f
= os
.popen("git show --raw --pretty=full %s" % rev
, 'r')
89 m
= re
.match(r
"^:.*[MAD]\s+(.+)$", line
)
91 logging
.debug("Got file: %s" % m
.group(1))
92 files
.append(m
.group(1))
95 m
= re
.match(r
"^Author:\s+(.+)$", line
)
97 logging
.debug("Got author: %s" % m
.group(1))
100 if re
.match(r
"^Merge: .*$", line
):
101 files
.append('merge')
106 logging
.warning("git show exited with status %d" % status
)
109 def gen_changes(input, branch
):
111 line
= input.readline()
115 logging
.debug("Change: %s" % line
)
117 m
= re
.match(r
"^([0-9a-f]+) (.*)$", line
.strip())
118 c
= {'revision': m
.group(1),
119 'comments': m
.group(2),
122 grab_commit_info(c
, m
.group(1))
126 def gen_create_branch_changes(newrev
, refname
, branch
):
127 # A new branch has been created. Generate changes for everything
128 # up to `newrev' which does not exist in any branch but `refname'.
130 # Note that this may be inaccurate if two new branches are created
131 # at the same time, pointing to the same commit, or if there are
132 # commits that only exists in a common subset of the new branches.
134 logging
.info("Branch `%s' created" % branch
)
136 f
= os
.popen("git rev-parse --not --branches"
137 + "| grep -v $(git rev-parse %s)" % refname
138 + "| git rev-list --reverse --pretty=oneline --stdin %s" % newrev
,
141 gen_changes(f
, branch
)
145 logging
.warning("git rev-list exited with status %d" % status
)
148 def gen_update_branch_changes(oldrev
, newrev
, refname
, branch
):
149 # A branch has been updated. If it was a fast-forward update,
150 # generate Change events for everything between oldrev and newrev.
152 # In case of a forced update, first generate a "fake" Change event
153 # rewinding the branch to the common ancestor of oldrev and
154 # newrev. Then, generate Change events for each commit between the
155 # common ancestor and newrev.
157 logging
.info("Branch `%s' updated %s .. %s"
158 % (branch
, oldrev
[:8], newrev
[:8]))
160 baserev
= commands
.getoutput("git merge-base %s %s" % (oldrev
, newrev
))
161 logging
.debug("oldrev=%s newrev=%s baserev=%s" % (oldrev
, newrev
, baserev
))
162 if baserev
!= oldrev
:
163 c
= {'revision': baserev
,
164 'comments': "Rewind branch",
168 logging
.info("Branch %s was rewound to %s" % (branch
, baserev
[:8]))
170 f
= os
.popen("git diff --raw %s..%s" % (oldrev
, baserev
), 'r')
176 file = re
.match(r
"^:.*[MAD]\s*(.+)$", line
).group(1)
177 logging
.debug(" Rewound file: %s" % file)
182 logging
.warning("git diff exited with status %d" % status
)
188 if newrev
!= baserev
:
190 f
= os
.popen("git rev-list --reverse --pretty=oneline %s..%s"
191 % (baserev
, newrev
), 'r')
192 gen_changes(f
, branch
)
196 logging
.warning("git rev-list exited with status %d" % status
)
203 def process_changes():
204 # Read branch updates from stdin and generate Change events
206 line
= sys
.stdin
.readline()
210 [oldrev
, newrev
, refname
] = line
.split(None, 2)
212 # We only care about regular heads, i.e. branches
213 m
= re
.match(r
"^refs\/heads\/(.+)$", refname
)
215 logging
.info("Ignoring refname `%s': Not a branch" % refname
)
220 # Find out if the branch was created, deleted or updated. Branches
221 # being deleted aren't really interesting.
222 if re
.match(r
"^0*$", newrev
):
223 logging
.info("Branch `%s' deleted, ignoring" % branch
)
225 elif re
.match(r
"^0*$", oldrev
):
226 gen_create_branch_changes(newrev
, refname
, branch
)
228 gen_update_branch_changes(oldrev
, newrev
, refname
, branch
)
230 # Submit the changes, if any
232 logging
.warning("No changes found")
235 host
, port
= master
.split(':')
238 f
= pb
.PBClientFactory()
239 d
= f
.login(credentials
.UsernamePassword("change", "changepw"))
240 reactor
.connectTCP(host
, port
, f
)
242 d
.addErrback(connectFailed
)
243 d
.addCallback(connected
)
250 parser
= OptionParser()
251 parser
.add_option("-l", "--logfile", action
="store", type="string",
252 help="Log to the specified file")
253 parser
.add_option("-v", "--verbose", action
="count",
254 help="Be more verbose. Ignored if -l is not specified.")
255 master_help
= ("Build master to push to. Default is %(master)s" %
256 { 'master' : master
})
257 parser
.add_option("-m", "--master", action
="store", type="string",
259 options
, args
= parser
.parse_args()
263 # Log errors and critical messages to stderr. Optionally log
264 # information to a file as well (we'll set that up later.)
265 stderr
= logging
.StreamHandler(sys
.stderr
)
266 fmt
= logging
.Formatter("git_buildbot: %(levelname)s: %(message)s")
267 stderr
.setLevel(logging
.ERROR
)
268 stderr
.setFormatter(fmt
)
269 logging
.getLogger().addHandler(stderr
)
270 logging
.getLogger().setLevel(logging
.DEBUG
)
273 options
= parse_options()
274 level
= logging
.WARNING
276 level
-= 10 * options
.verbose
281 logfile
= logging
.FileHandler(options
.logfile
)
282 logfile
.setLevel(level
)
283 fmt
= logging
.Formatter("%(asctime)s %(levelname)s: %(message)s")
284 logfile
.setFormatter(fmt
)
285 logging
.getLogger().addHandler(logfile
)
288 master
=options
.master
294 logging
.exception("Unhandled exception")