3
# this requires python >=2.3 for the 'sets' module.
5
# The sets.py from python-2.3 appears to work fine under python2.2 . To
6
# install this script on a host with only python2.2, copy
7
# /usr/lib/python2.3/sets.py from a newer python into somewhere on your
8
# PYTHONPATH, then edit the #! line above to invoke python2.2
10
# python2.1 is right out
12
# If you run this program as part of your SVN post-commit hooks, it will
13
# deliver Change notices to a buildmaster that is running a PBChangeSource
16
# edit your svn-repository/hooks/post-commit file, and add lines that look
20
# set up PYTHONPATH to contain Twisted/buildbot perhaps, if not already
24
/path/to/svn_buildbot.py --repository "$REPOS" --revision "$REV" \
25
--bbserver localhost --bbport 9989
34
# We have hackish "-d" handling here rather than in the Options
35
# subclass below because a common error will be to not have twisted in
36
# PYTHONPATH; we want to be able to print that error to the log if
37
# debug mode is on, so we set it up before the imports.
42
i = sys.argv.index('-d')
53
from twisted.internet import defer, reactor
54
from twisted.python import usage
55
from twisted.spread import pb
56
from twisted.cred import credentials
59
class Options(usage.Options):
61
['repository', 'r', None,
62
"The repository that was changed."],
63
['slave-repo', 'c', None, "In case the repository differs for the slaves."],
64
['revision', 'v', None,
65
"The revision that we want to examine (default: latest)"],
66
['bbserver', 's', 'localhost',
67
"The hostname of the server that buildbot is running on"],
69
"The port that buildbot is listening on"],
70
['include', 'f', None,
72
Search the list of changed files for this regular expression, and if there is
73
at least one match notify buildbot; otherwise buildbot will not do a build.
74
You may provide more than one -f argument to try multiple
75
patterns. If no filter is given, buildbot will always be notified.'''],
76
['filter', 'f', None, "Same as --include. (Deprecated)"],
77
['exclude', 'F', None,
79
The inverse of --filter. Changed files matching this expression will never
80
be considered for a build.
81
You may provide more than one -F argument to try multiple
82
patterns. Excludes override includes, that is, patterns that match both an
83
include and an exclude will be excluded.'''],
84
['encoding', 'e', "utf8",
85
"The encoding of the strings from subversion (default: utf8)" ],
86
['project', 'P', None, "The project for the source."]
89
['dryrun', 'n', "Do not actually send changes"],
93
usage.Options.__init__(self)
96
self['includes'] = None
97
self['excludes'] = None
99
def opt_include(self, arg):
100
self._includes.append('.*%s.*' % (arg, ))
102
opt_filter = opt_include
104
def opt_exclude(self, arg):
105
self._excludes.append('.*%s.*' % (arg, ))
107
def postOptions(self):
108
if self['repository'] is None:
109
raise usage.error("You must pass --repository")
111
self['includes'] = '(%s)' % ('|'.join(self._includes), )
113
self['excludes'] = '(%s)' % ('|'.join(self._excludes), )
116
def split_file_dummy(changed_file):
117
"""Split the repository-relative filename into a tuple of (branchname,
118
branch_relative_filename). If you have no branches, this should just
119
return (None, changed_file).
121
return (None, changed_file)
124
# this version handles repository layouts that look like:
125
# trunk/files.. -> trunk
126
# branches/branch1/files.. -> branches/branch1
127
# branches/branch2/files.. -> branches/branch2
131
def split_file_branches(changed_file):
132
pieces = changed_file.split(os.sep)
133
if pieces[0] == 'branches':
134
return (os.path.join(*pieces[:2]),
135
os.path.join(*pieces[2:]))
136
if pieces[0] == 'trunk':
137
return (pieces[0], os.path.join(*pieces[1:]))
138
## there are other sibilings of 'trunk' and 'branches'. Pretend they are
139
## all just funny-named branches, and let the Schedulers ignore them.
140
#return (pieces[0], os.path.join(*pieces[1:]))
142
raise RuntimeError("cannot determine branch for '%s'" % changed_file)
145
split_file = split_file_dummy
150
def getChanges(self, opts):
151
"""Generate and stash a list of Change dictionaries, ready to be sent
152
to the buildmaster's PBChangeSource."""
154
# first we extract information about the files that were changed
155
repo = opts['repository']
156
slave_repo = opts['slave-repo'] or repo
160
rev_arg = '-r %s' % (opts['revision'], )
161
changed = commands.getoutput('svnlook changed %s "%s"' % (
162
rev_arg, repo)).split('\n')
163
# the first 4 columns can contain status information
164
changed = [x[4:] for x in changed]
166
message = commands.getoutput('svnlook log %s "%s"' % (rev_arg, repo))
167
who = commands.getoutput('svnlook author %s "%s"' % (rev_arg, repo))
168
revision = opts.get('revision')
169
if revision is not None:
170
revision = str(int(revision))
172
# see if we even need to notify buildbot by looking at filters first
173
changestring = '\n'.join(changed)
174
fltpat = opts['includes']
176
included = sets.Set(re.findall(fltpat, changestring))
178
included = sets.Set(changed)
180
expat = opts['excludes']
182
excluded = sets.Set(re.findall(expat, changestring))
184
excluded = sets.Set([])
185
if len(included.difference(excluded)) == 0:
188
Buildbot was not interested, no changes matched any of these filters:\n %s
189
or all the changes matched these exclusions:\n %s\
190
""" % (fltpat, expat)
193
# now see which branches are involved
194
files_per_branch = {}
196
branch, filename = split_file(f)
197
if branch in files_per_branch.keys():
198
files_per_branch[branch].append(filename)
200
files_per_branch[branch] = [filename]
202
# now create the Change dictionaries
204
encoding = opts['encoding']
205
for branch in files_per_branch.keys():
206
d = {'who': unicode(who, encoding=encoding),
207
'repository': unicode(slave_repo, encoding=encoding),
208
'comments': unicode(message, encoding=encoding),
209
'revision': revision,
210
'project' : unicode(opts['project'] or "", encoding=encoding),
214
d['branch'] = unicode(branch, encoding=encoding)
219
for file in files_per_branch[branch]:
220
files.append(unicode(file, encoding=encoding))
227
def sendChanges(self, opts, changes):
228
pbcf = pb.PBClientFactory()
229
reactor.connectTCP(opts['bbserver'], int(opts['bbport']), pbcf)
230
d = pbcf.login(credentials.UsernamePassword('change', 'changepw'))
231
d.addCallback(self.sendAllChanges, changes)
234
def sendAllChanges(self, remote, changes):
235
dl = [remote.callRemote('addChange', change)
236
for change in changes]
237
return defer.DeferredList(dl)
243
except usage.error, ue:
245
print "%s: %s" % (sys.argv[0], ue)
248
changes = self.getChanges(opts)
250
for i, c in enumerate(changes):
251
print "CHANGE #%d" % (i+1)
255
print "[%10s]: %s" % (k, c[k])
256
print "*NOT* sending any changes"
259
d = self.sendChanges(opts, changes)
262
print "quitting! because", why
270
d.addCallback(quit, "SUCCESS")
272
reactor.callLater(60, quit, "TIMEOUT")
276
if __name__ == '__main__':