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>
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
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
35 def connectFailed(error
):
36 logging
.error("Could not connect to %s: %s"
37 % (master
, error
.getErrorMessage()))
40 def addChange(dummy
, remote
, changei
):
41 logging
.debug("addChange %s, %s" % (repr(remote
), repr(changei
)))
45 remote
.broker
.transport
.loseConnection()
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
)
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')
70 m
= re
.match(r
"^:.*[MAD]\s+(.+)$", line
)
72 logging
.debug("Got file: %s" % m
.group(1))
73 files
.append(m
.group(1))
76 m
= re
.match(r
"^Commit:\s+(.+)$", line
)
78 logging
.debug("Got committer: %s" % m
.group(1))
84 logging
.warning("git-show exited with status %d" % status
)
86 def gen_changes(input, branch
):
88 line
= input.readline()
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),
97 grab_commit_info(c
, m
.group(1))
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
,
115 gen_changes(f
, branch
)
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]))
141 f
= os
.popen("git-diff --raw %s..%s" % (oldrev
, baserev
), 'r')
147 file = re
.match(r
"^:.*[MAD]\s*(.+)$", line
).group(1)
148 logging
.debug(" Rewound file: %s" % file)
153 logging
.warning("git-diff exited with status %d" % status
)
159 if newrev
!= baserev
:
161 f
= os
.popen("git-rev-list --reverse --pretty=oneline %s..%s"
162 % (baserev
, newrev
), 'r')
163 gen_changes(f
, branch
)
167 logging
.warning("git-rev-list exited with status %d" % status
)
172 def process_changes():
173 # Read branch updates from stdin and generate Change events
175 line
= sys
.stdin
.readline()
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
)
184 logging
.info("Ignoring refname `%s': Not a branch" % refname
)
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
)
194 elif re
.match(r
"^0*$", oldrev
):
195 gen_create_branch_changes(newrev
, refname
, branch
)
197 gen_update_branch_changes(oldrev
, newrev
, refname
, branch
)
199 # Submit the changes, if any
201 logging
.warning("No changes found")
204 host
, port
= master
.split(':')
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
)
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()
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
)
236 options
= parse_options()
237 level
= logging
.WARNING
239 level
-= 10 * options
.verbose
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
)
252 logging
.exception("Unhandled exception")