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 ['revision', 'v', None,
64 "The revision that we want to examine (default: latest)"],
65 ['bbserver', 's', 'localhost',
66 "The hostname of the server that buildbot is running on"],
68 "The port that buildbot is listening on"],
69 ['include', 'f', None,
71 Search the list of changed files for this regular expression, and if there is
72 at least one match notify buildbot; otherwise buildbot will not do a build.
73 You may provide more than one -f argument to try multiple
74 patterns. If no filter is given, buildbot will always be notified.'''],
75 ['filter', 'f', None, "Same as --include. (Deprecated)"],
76 ['exclude', 'F', None,
78 The inverse of --filter. Changed files matching this expression will never
79 be considered for a build.
80 You may provide more than one -F argument to try multiple
81 patterns. Excludes override includes, that is, patterns that match both an
82 include and an exclude will be excluded.'''],
85 ['dryrun', 'n', "Do not actually send changes"],
89 usage
.Options
.__init
__(self
)
92 self
['includes'] = None
93 self
['excludes'] = None
95 def opt_include(self
, arg
):
96 self
._includes
.append('.*%s.*' % (arg
, ))
98 opt_filter
= opt_include
100 def opt_exclude(self
, arg
):
101 self
._excludes
.append('.*%s.*' % (arg
, ))
103 def postOptions(self
):
104 if self
['repository'] is None:
105 raise usage
.error("You must pass --repository")
107 self
['includes'] = '(%s)' % ('|'.join(self
._includes
), )
109 self
['excludes'] = '(%s)' % ('|'.join(self
._excludes
), )
112 def split_file_dummy(changed_file
):
113 """Split the repository-relative filename into a tuple of (branchname,
114 branch_relative_filename). If you have no branches, this should just
115 return (None, changed_file).
117 return (None, changed_file
)
120 # this version handles repository layouts that look like:
121 # trunk/files.. -> trunk
122 # branches/branch1/files.. -> branches/branch1
123 # branches/branch2/files.. -> branches/branch2
127 def split_file_branches(changed_file
):
128 pieces
= changed_file
.split(os
.sep
)
129 if pieces
[0] == 'branches':
130 return (os
.path
.join(*pieces
[:2]),
131 os
.path
.join(*pieces
[2:]))
132 if pieces
[0] == 'trunk':
133 return (pieces
[0], os
.path
.join(*pieces
[1:]))
134 ## there are other sibilings of 'trunk' and 'branches'. Pretend they are
135 ## all just funny-named branches, and let the Schedulers ignore them.
136 #return (pieces[0], os.path.join(*pieces[1:]))
138 raise RuntimeError("cannot determine branch for '%s'" % changed_file
)
141 split_file
= split_file_dummy
146 def getChanges(self
, opts
):
147 """Generate and stash a list of Change dictionaries, ready to be sent
148 to the buildmaster's PBChangeSource."""
150 # first we extract information about the files that were changed
151 repo
= opts
['repository']
155 rev_arg
= '-r %s' % (opts
['revision'], )
156 changed
= commands
.getoutput('svnlook changed %s "%s"' % (
157 rev_arg
, repo
)).split('\n')
158 # the first 4 columns can contain status information
159 changed
= [x
[4:] for x
in changed
]
161 message
= commands
.getoutput('svnlook log %s "%s"' % (rev_arg
, repo
))
162 who
= commands
.getoutput('svnlook author %s "%s"' % (rev_arg
, repo
))
163 revision
= opts
.get('revision')
164 if revision
is not None:
165 revision
= int(revision
)
167 # see if we even need to notify buildbot by looking at filters first
168 changestring
= '\n'.join(changed
)
169 fltpat
= opts
['includes']
171 included
= sets
.Set(re
.findall(fltpat
, changestring
))
173 included
= sets
.Set(changed
)
175 expat
= opts
['excludes']
177 excluded
= sets
.Set(re
.findall(expat
, changestring
))
179 excluded
= sets
.Set([])
180 if len(included
.difference(excluded
)) == 0:
183 Buildbot was not interested, no changes matched any of these filters:\n %s
184 or all the changes matched these exclusions:\n %s\
185 """ % (fltpat
, expat
)
188 # now see which branches are involved
189 files_per_branch
= {}
191 branch
, filename
= split_file(f
)
192 if branch
in files_per_branch
.keys():
193 files_per_branch
[branch
].append(filename
)
195 files_per_branch
[branch
] = [filename
]
197 # now create the Change dictionaries
199 for branch
in files_per_branch
.keys():
202 'files': files_per_branch
[branch
],
204 'revision': revision
}
209 def sendChanges(self
, opts
, changes
):
210 pbcf
= pb
.PBClientFactory()
211 reactor
.connectTCP(opts
['bbserver'], int(opts
['bbport']), pbcf
)
212 d
= pbcf
.login(credentials
.UsernamePassword('change', 'changepw'))
213 d
.addCallback(self
.sendAllChanges
, changes
)
216 def sendAllChanges(self
, remote
, changes
):
217 dl
= [remote
.callRemote('addChange', change
)
218 for change
in changes
]
219 return defer
.DeferredList(dl
)
225 except usage
.error
, ue
:
227 print "%s: %s" % (sys
.argv
[0], ue
)
230 changes
= self
.getChanges(opts
)
232 for i
, c
in enumerate(changes
):
233 print "CHANGE #%d" % (i
+1)
237 print "[%10s]: %s" % (k
, c
[k
])
238 print "*NOT* sending any changes"
241 d
= self
.sendChanges(opts
, changes
)
244 print "quitting! because", why
252 d
.addCallback(quit
, "SUCCESS")
254 reactor
.callLater(60, quit
, "TIMEOUT")
258 if __name__
== '__main__':