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" --bbserver localhost --bbport 9989
27 import commands
, sys
, os
31 # We have hackish "-d" handling here rather than in the Options
32 # subclass below because a common error will be to not have twisted in
33 # PYTHONPATH; we want to be able to print that error to the log if
34 # debug mode is on, so we set it up before the imports.
39 i
= sys
.argv
.index('-d')
49 from twisted
.internet
import defer
, reactor
50 from twisted
.python
import usage
51 from twisted
.spread
import pb
52 from twisted
.cred
import credentials
54 class Options(usage
.Options
):
56 ['repository', 'r', None,
57 "The repository that was changed."],
58 ['revision', 'v', None,
59 "The revision that we want to examine (default: latest)"],
60 ['bbserver', 's', 'localhost',
61 "The hostname of the server that buildbot is running on"],
63 "The port that buildbot is listening on"],
64 ['include', 'f', None,
66 Search the list of changed files for this regular expression, and if there is
67 at least one match notify buildbot; otherwise buildbot will not do a build.
68 You may provide more than one -f argument to try multiple
69 patterns. If no filter is given, buildbot will always be notified.'''],
70 ['filter', 'f', None, "Same as --include. (Deprecated)"],
71 ['exclude', 'F', None,
73 The inverse of --filter. Changed files matching this expression will never
74 be considered for a build.
75 You may provide more than one -F argument to try multiple
76 patterns. Excludes override includes, that is, patterns that match both an
77 include and an exclude will be excluded.'''],
80 ['dryrun', 'n', "Do not actually send changes"],
84 usage
.Options
.__init
__(self
)
87 self
['includes'] = None
88 self
['excludes'] = None
90 def opt_include(self
, arg
):
91 self
._includes
.append('.*%s.*' % (arg
,))
92 opt_filter
= opt_include
94 def opt_exclude(self
, arg
):
95 self
._excludes
.append('.*%s.*' % (arg
,))
97 def postOptions(self
):
98 if self
['repository'] is None:
99 raise usage
.error("You must pass --repository")
101 self
['includes'] = '(%s)' % ('|'.join(self
._includes
),)
103 self
['excludes'] = '(%s)' % ('|'.join(self
._excludes
),)
105 def split_file_dummy(changed_file
):
106 """Split the repository-relative filename into a tuple of (branchname,
107 branch_relative_filename). If you have no branches, this should just
108 return (None, changed_file).
110 return (None, changed_file
)
112 # this version handles repository layouts that look like:
113 # trunk/files.. -> trunk
114 # branches/branch1/files.. -> branches/branch1
115 # branches/branch2/files.. -> branches/branch2
117 def split_file_branches(changed_file
):
118 pieces
= changed_file
.split(os
.sep
)
119 if pieces
[0] == 'branches':
120 return (os
.path
.join(*pieces
[:2]),
121 os
.path
.join(*pieces
[2:]))
122 if pieces
[0] == 'trunk':
123 return (pieces
[0], os
.path
.join(*pieces
[1:]))
124 ## there are other sibilings of 'trunk' and 'branches'. Pretend they are
125 ## all just funny-named branches, and let the Schedulers ignore them.
126 #return (pieces[0], os.path.join(*pieces[1:]))
128 raise RuntimeError("cannot determine branch for '%s'" % changed_file
)
130 split_file
= split_file_dummy
135 def getChanges(self
, opts
):
136 """Generate and stash a list of Change dictionaries, ready to be sent
137 to the buildmaster's PBChangeSource."""
139 # first we extract information about the files that were changed
140 repo
= opts
['repository']
144 rev_arg
= '-r %s' % (opts
['revision'],)
145 changed
= commands
.getoutput('svnlook changed %s "%s"' % (rev_arg
,
148 # the first 4 columns can contain status information
149 changed
= [x
[4:] for x
in changed
]
151 message
= commands
.getoutput('svnlook log %s "%s"' % (rev_arg
, repo
))
152 who
= commands
.getoutput('svnlook author %s "%s"' % (rev_arg
, repo
))
153 revision
= opts
.get('revision')
154 if revision
is not None:
155 revision
= int(revision
)
157 # see if we even need to notify buildbot by looking at filters first
158 changestring
= '\n'.join(changed
)
159 fltpat
= opts
['includes']
161 included
= sets
.Set(re
.findall(fltpat
, changestring
))
163 included
= sets
.Set(changed
)
165 expat
= opts
['excludes']
167 excluded
= sets
.Set(re
.findall(expat
, changestring
))
169 excluded
= sets
.Set([])
170 if len(included
.difference(excluded
)) == 0:
173 Buildbot was not interested, no changes matched any of these filters:\n %s
174 or all the changes matched these exclusions:\n %s\
175 """ % (fltpat
, expat
)
178 # now see which branches are involved
179 files_per_branch
= {}
181 branch
, filename
= split_file(f
)
182 if files_per_branch
.has_key(branch
):
183 files_per_branch
[branch
].append(filename
)
185 files_per_branch
[branch
] = [filename
]
187 # now create the Change dictionaries
189 for branch
in files_per_branch
.keys():
192 'files': files_per_branch
[branch
],
194 'revision': revision
}
199 def sendChanges(self
, opts
, changes
):
200 pbcf
= pb
.PBClientFactory()
201 reactor
.connectTCP(opts
['bbserver'], int(opts
['bbport']), pbcf
)
202 d
= pbcf
.login(credentials
.UsernamePassword('change', 'changepw'))
203 d
.addCallback(self
.sendAllChanges
, changes
)
206 def sendAllChanges(self
, remote
, changes
):
207 dl
= [remote
.callRemote('addChange', change
)
208 for change
in changes
]
209 return defer
.DeferredList(dl
)
215 except usage
.error
, ue
:
217 print "%s: %s" % (sys
.argv
[0], ue
)
220 changes
= self
.getChanges(opts
)
222 for i
,c
in enumerate(changes
):
223 print "CHANGE #%d" % (i
+1)
227 print "[%10s]: %s" % (k
, c
[k
])
228 print "*NOT* sending any changes"
231 d
= self
.sendChanges(opts
, changes
)
234 print "quitting! because", why
242 d
.addCallback(quit
, "SUCCESS")
244 reactor
.callLater(60, quit
, "TIMEOUT")
247 if __name__
== '__main__':