2
# -*- coding:utf-8;mode:python;mode:font-lock -*-
5
# Utility for Subversion commit hook scripts
6
# This script enforces certain coding guidelines
8
# Copyright (c) 2005 Wilfredo Sanchez Vega <wsanchez@wsanchez.net>.
11
# Permission to use, copy, modify, and distribute this software for any
12
# purpose with or without fee is hereby granted, provided that the above
13
# copyright notice and this permission notice appear in all copies.
15
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL
16
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
17
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
18
# AUTHORS BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
19
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
20
# PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
21
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
22
# PERFORMANCE OF THIS SOFTWARE.
31
# FIXME: Should probably retool this using python bindings, not svnlook
35
Enforcer is a utility which can be used in a Subversion pre-commit
36
hook script to enforce various requirements which a repository
37
administrator would like to impose on data coming into the repository.
39
A couple of example scenarios:
41
- In a Java project I work on, we use log4j extensively. Use of
42
System.out.println() bypasses the control that we get from log4j,
43
so we would like to discourage the addition of println calls in our
46
We want to deny any commits that add a println into the code. The
47
world being full of exceptions, we do need a way to allow some uses
48
of println, so we will allow it if the line of code that calls
49
println ends in a comment that says it is ok:
51
System.out.println("No log4j here"); // (authorized)
53
We also do not (presently) want to refuse a commit to a file which
54
already has a println in it. There are too many already in the
55
code and a given developer may not have time to fix them up before
56
commiting an unrelated change to a file.
58
- The above project uses WebObjects, and you can enable debugging in
59
a WebObjects component by turning on the WODebug flag in the
60
component WOD file. That is great for debugging, but massively
61
bloats the log files when the application is deployed.
63
We want to disable any commit of a file enabling WODebug,
64
regardless of whether the committer made the change or not; these
65
have to be cleaned up before any successful commit.
67
What this script does is it uses svnlook to peek into the transaction
68
is progress. As it sifts through the transaction, it calls out to a
69
set of hooks which allow the repository administrator to examine what
70
is going on and decide whether it is acceptable. Hooks may be written
71
(in Python) into a configuration file. If the hook raises an
72
exception, enforcer will exit with an error status (and presumably the
73
commit will be denied by th pre-commit hook). The following hooks are
76
verify_file_added(filename)
77
- called when a file is added.
79
verify_file_removed(fielname)
80
- called when a file is removed.
82
verify_file_copied(destination_filename, source_filename)
83
- called when a file is copied.
85
verify_file_modified(filename)
86
- called when a file is modified.
88
verify_line_added(filename, line)
89
- called for each line that is added to a file.
90
(verify_file_modified() will have been called on the file
93
verify_line_removed(filename, line)
94
- called for each line that is removed from a file.
95
(verify_file_modified() will have been called on the file
98
verify_property_line_added(filename, property, line)
99
- called for each line that is added to a property on a file.
101
verify_property_line_removed(filename, property, line)
102
- called for each line that is removed from a property on a file.
104
In addition, these functions are available to be called from within a
108
- Returns an open file-like object from which the data of the given
109
file (as available in the transaction being processed) can be
112
In our example scenarios, we can deny the addition of println calls by
113
hooking into verify_line_added(): if the file is a Java file, and the
114
added line calls println, raise an exception.
116
Similarly, we can deny the commit of any WOD file enabling WODebug by
117
hooking into verify_file_modified(): open the file using open_file(),
118
then raise if WODebug is enabled anywhere in the file.
120
Note that verify_file_modified() is called once per modified file,
121
whereas verify_line_added() and verify_line_removed() may each be
122
called zero or many times for each modified file, depending on the
123
change. This makes verify_file_modified() appropriate for checking
124
the entire file and the other two appropriate for checking specific
127
These example scenarios are implemented in the provided example
128
configuration file "enforcer.conf".
130
When writing hooks, it is usually easier to test the hooks on commited
131
transactions already in the repository, rather than installing the
132
hook and making commits to test the them. Enforcer allows you to
133
specify either a transaction ID (for use in a hook script) or a
134
revision number (for testing). You can then, for example, find a
135
revision that you would like to have blocked (or not) and test your
136
hooks against that revision.
138
__author__ = "Wilfredo Sanchez Vega <wsanchez@wsanchez.net>"
141
# Handle command line
144
program = os.path.split(sys.argv[0])[1]
154
print "usage: %s [options] repository config" % program
156
print "\t-d, --debug Print debugging output; use twice for more"
157
print "\t-r, --revision rev Specify revision to check"
158
print "\t-t, --transaction txn Specify transaction to check"
159
print "Exactly one of --revision or --transaction is required"
165
(optargs, args) = getopt.getopt(sys.argv[1:], "dt:r:", ["debug", "transaction=", "revision="])
166
except getopt.GetoptError, e:
169
for optarg in optargs:
171
if opt in ("-d", "--debug" ): debug += 1
172
elif opt in ("-t", "--transaction"): transaction = arg
173
elif opt in ("-r", "--revision" ): revision = arg
175
if transaction and revision:
176
usage("Cannot specify both transaction and revision to check")
177
if not transaction and not revision:
178
usage("Must specify transaction or revision to check")
180
if not len(args): usage("No repository")
181
repository = args.pop(0)
183
if not len(args): usage("No config")
184
configuration_filename = args.pop(0)
186
if len(args): usage("Too many arguments")
190
# All rule enforcement goes in these routines
193
def open_file(filename):
195
Retrieves the contents of the given file.
197
cat_cmd = [ "svnlook", "cat", None, repository, filename ]
199
if transaction: cat_cmd[2] = "--transaction=" + transaction
200
elif revision: cat_cmd[2] = "--revision=" + revision
201
else: raise ValueError("No transaction or revision")
203
cat_out, cat_in = popen2.popen2(cat_cmd)
208
def verify_file_added(filename):
210
Here we verify file additions which may not meet our requirements.
212
if debug: print "Added file %r" % filename
213
if configuration.has_key("verify_file_added"):
214
configuration["verify_file_added"](filename)
216
def verify_file_removed(filename):
218
Here we verify file removals which may not meet our requirements.
220
if debug: print "Removed file %r" % filename
221
if configuration.has_key("verify_file_removed"):
222
configuration["verify_file_removed"](filename)
224
def verify_file_copied(destination_filename, source_filename):
226
Here we verify file copies which may not meet our requirements.
228
if debug: print "Copied %r to %r" % (source_filename, destination_filename)
229
if configuration.has_key("verify_file_copied"):
230
configuration["verify_file_copied"](destination_filename, source_filename)
232
def verify_file_modified(filename):
234
Here we verify files which may not meet our requirements.
235
Any failure, even if not due to the specific changes in the commit
238
if debug: print "Modified file %r" % filename
239
if configuration.has_key("verify_file_modified"):
240
configuration["verify_file_modified"](filename)
242
def verify_line_added(filename, line):
244
Here we verify new lines of code which may not meet our requirements.
245
Code not changed as part of this commit is not verified.
247
if configuration.has_key("verify_line_added"):
248
configuration["verify_line_added"](filename, line)
250
def verify_line_removed(filename, line):
252
Here we verify removed lines of code which may not meet our requirements.
253
Code not changed as part of this commit is not verified.
255
if configuration.has_key("verify_line_removed"):
256
configuration["verify_line_removed"](filename, line)
258
def verify_property_line_added(filename, property, line):
260
Here we verify added property lines which may not meet our requirements.
261
Code not changed as part of this commit is not verified.
263
if debug: print "Add %s::%s: %s" % (filename, property, line)
264
if configuration.has_key("verify_property_line_added"):
265
configuration["verify_property_line_added"](filename, property, line)
267
def verify_property_line_removed(filename, property, line):
269
Here we verify removed property lines which may not meet our requirements.
270
Code not changed as part of this commit is not verified.
272
if debug: print "Del %s::%s: %s" % (filename, property, line)
273
if configuration.has_key("verify_property_line_removed"):
274
configuration["verify_property_line_removed"](filename, property, line)
280
configuration = {"open_file": open_file}
281
execfile(configuration_filename, configuration, configuration)
283
diff_cmd = [ "svnlook", "diff", None, repository ]
285
if transaction: diff_cmd[2] = "--transaction=" + transaction
286
elif revision: diff_cmd[2] = "--revision=" + revision
287
else: raise ValueError("No transaction or revision")
289
diff_out, diff_in = popen2.popen2(diff_cmd)
296
# This is the svnlook output parser
298
for line in diff_out:
299
if line[-1] == "\n": line = line[:-1] # Zap trailing newline
302
# r2266: Added text files, property changes
303
# r18923: Added, deleted, modified text files
304
# r25692: Copied files
305
# r7758: Added binary files
307
if debug > 1: print "%4d: %s" % (state, line) # Useful for testing parser problems
309
if state is -1: # Used for testing new states: print whatever is left
313
if state in (0, 100, 300): # Initial state or in a state that may return to initial state
314
if state is 0 and not line: continue
316
colon = line.find(":")
318
if colon != -1 and len(line) > colon + 2:
319
action = line[:colon]
320
filename = line[colon+2:]
324
"Added", "Deleted", "Copied",
325
"Property changes on",
327
if action == "Modified": verify_file_modified(filename)
328
elif action == "Added" : verify_file_added (filename)
329
elif action == "Deleted" : verify_file_removed (filename)
330
elif action == "Copied":
331
i = filename.find(" (from rev ")
332
destination_filename = filename[:i]
333
filename = filename[i:]
335
i = filename.find(", ")
336
assert filename[-1] == ")"
337
source_filename = filename[i+2:-1]
339
verify_file_copied(destination_filename, source_filename)
341
filename = destination_filename
343
if action == "Modified" : state = 10
344
elif action == "Added" : state = 10
345
elif action == "Deleted" : state = 10
346
elif action == "Copied" : state = 20
347
elif action == "Property changes on": state = 30
348
else: raise AssertionError("Unknown action")
350
current_filename = filename
351
current_property = None
355
assert state in (100, 300)
357
if state is 10: # Expecting a bar (follows "Modified:" line)
358
assert line == "=" * 67
362
if state is 11: # Expecting left file info (follows bar)
363
if line == "": state = 0
364
elif line == "(Binary files differ)": state = 0
365
elif line.startswith("--- "): state = 12
366
else: raise AssertionError("Expected left file info, got: %r" % line)
370
if state is 12: # Expecting right file info (follows left file info)
371
assert line.startswith("+++ " + current_filename)
375
if state is 20: # Expecting a bar or blank (follows "Copied:" line)
377
# r25692: Copied and not modified (blank)
378
# r26613: Copied and modified (bar)
381
elif line == "=" * 67:
384
raise AssertionError("After Copied: line, neither bar nor blank: %r" % line)
387
if state is 100: # Expecting diff data
388
for c, verify in (("-", verify_line_removed), ("+", verify_line_added)):
389
if len(line) >= 1 and line[0] == c:
390
try: verify(current_filename, line[1:])
392
sys.stderr.write(str(e))
393
sys.stderr.write("\n")
399
(len(line) >= 4 and line[:2] == "@@" == line[-2:]) or
400
(len(line) >= 1 and line[0] == " ") or
401
line == "\\ No newline at end of file"
405
raise AssertionError("Expected diff data, got: %r" % line)
409
if state is 30: # Expecting a bar (follows "Property changes on:" line)
410
assert line == "_" * 67
414
if state is 31: # Expecting property name (follows bar)
415
assert line.startswith("Name: ")
417
# Fall through to state 300
420
if line.startswith("Name: "):
421
current_property = line[6:]
424
for prefix, verify in (
425
(" - ", verify_property_line_removed),
426
(" + ", verify_property_line_added)
428
if line.startswith(prefix):
429
try: verify(current_filename, current_property, line[5:])
431
sys.stderr.write(str(e))
432
sys.stderr.write("\n")
436
if not line: continue
438
raise AssertionError("Expected property diff data, got: %r" % line)
442
raise AssertionError("Unparsed line: %r" % line)
444
if debug: print "Commit is OK"
447
while diff_out.read(1024 * 1024 * 64): pass