| #!/usr/bin/python |
| # -*- coding:utf-8;mode:python;mode:font-lock -*- |
| |
| ## |
| # Utility for Subversion commit hook scripts |
| # This script enforces certain coding guidelines |
| ## |
| # Copyright (c) 2005 Wilfredo Sanchez Vega <wsanchez@wsanchez.net>. |
| # All rights reserved. |
| # |
| # Permission to use, copy, modify, and distribute this software for any |
| # purpose with or without fee is hereby granted, provided that the above |
| # copyright notice and this permission notice appear in all copies. |
| # |
| # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL |
| # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE |
| # AUTHORS BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL |
| # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR |
| # PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER |
| # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR |
| # PERFORMANCE OF THIS SOFTWARE. |
| ## |
| |
| import sys |
| import os |
| import getopt |
| import popen2 |
| |
| # |
| # FIXME: Should probably retool this using python bindings, not svnlook |
| # |
| |
| __doc__ = ''' |
| Enforcer is a utility which can be used in a Subversion pre-commit |
| hook script to enforce various requirements which a repository |
| administrator would like to impose on data coming into the repository. |
| |
| A couple of example scenarios: |
| |
| - In a Java project I work on, we use log4j extensively. Use of |
| System.out.println() bypasses the control that we get from log4j, |
| so we would like to discourage the addition of println calls in our |
| code. |
| |
| We want to deny any commits that add a println into the code. The |
| world being full of exceptions, we do need a way to allow some uses |
| of println, so we will allow it if the line of code that calls |
| println ends in a comment that says it is ok: |
| |
| System.out.println("No log4j here"); // (authorized) |
| |
| We also do not (presently) want to refuse a commit to a file which |
| already has a println in it. There are too many already in the |
| code and a given developer may not have time to fix them up before |
| commiting an unrelated change to a file. |
| |
| - The above project uses WebObjects, and you can enable debugging in |
| a WebObjects component by turning on the WODebug flag in the |
| component WOD file. That is great for debugging, but massively |
| bloats the log files when the application is deployed. |
| |
| We want to disable any commit of a file enabling WODebug, |
| regardless of whether the committer made the change or not; these |
| have to be cleaned up before any successful commit. |
| |
| What this script does is it uses svnlook to peek into the transaction |
| is progress. As it sifts through the transaction, it calls out to a |
| set of hooks which allow the repository administrator to examine what |
| is going on and decide whether it is acceptable. Hooks may be written |
| (in Python) into a configuration file. If the hook raises an |
| exception, enforcer will exit with an error status (and presumably the |
| commit will be denied by th pre-commit hook). The following hooks are |
| available: |
| |
| verify_file_added(filename) |
| - called when a file is added. |
| |
| verify_file_removed(filename) |
| - called when a file is removed. |
| |
| verify_file_copied(destination_filename, source_filename) |
| - called when a file is copied. |
| |
| verify_file_modified(filename) |
| - called when a file is modified. |
| |
| verify_line_added(filename, line) |
| - called for each line that is added to a file. |
| (verify_file_modified() will have been called on the file |
| beforehand) |
| |
| verify_line_removed(filename, line) |
| - called for each line that is removed from a file. |
| (verify_file_modified() will have been called on the file |
| beforehand) |
| |
| verify_property_line_added(filename, property, line) |
| - called for each line that is added to a property on a file. |
| |
| verify_property_line_removed(filename, property, line) |
| - called for each line that is removed from a property on a file. |
| |
| In addition, these functions are available to be called from within a |
| hook routine: |
| |
| open_file(filename) |
| - Returns an open file-like object from which the data of the given |
| file (as available in the transaction being processed) can be |
| read. |
| |
| In our example scenarios, we can deny the addition of println calls by |
| hooking into verify_line_added(): if the file is a Java file, and the |
| added line calls println, raise an exception. |
| |
| Similarly, we can deny the commit of any WOD file enabling WODebug by |
| hooking into verify_file_modified(): open the file using open_file(), |
| then raise if WODebug is enabled anywhere in the file. |
| |
| Note that verify_file_modified() is called once per modified file, |
| whereas verify_line_added() and verify_line_removed() may each be |
| called zero or many times for each modified file, depending on the |
| change. This makes verify_file_modified() appropriate for checking |
| the entire file and the other two appropriate for checking specific |
| changes to files. |
| |
| These example scenarios are implemented in the provided example |
| configuration file "enforcer.conf". |
| |
| When writing hooks, it is usually easier to test the hooks on committed |
| transactions already in the repository, rather than installing the |
| hook and making commits to test them. Enforcer allows you to |
| specify either a transaction ID (for use in a hook script) or a |
| revision number (for testing). You can then, for example, find a |
| revision that you would like to have blocked (or not) and test your |
| hooks against that revision. |
| ''' |
| __author__ = "Wilfredo Sanchez Vega <wsanchez@wsanchez.net>" |
| |
| ## |
| # Handle command line |
| ## |
| |
| program = os.path.split(sys.argv[0])[1] |
| debug = 0 |
| transaction = None |
| revision = None |
| |
| def usage(e=None): |
| if e: |
| print e |
| print "" |
| |
| print "usage: %s [options] repository config" % program |
| print "options:" |
| print "\t-d, --debug Print debugging output; use twice for more" |
| print "\t-r, --revision rev Specify revision to check" |
| print "\t-t, --transaction txn Specify transaction to check" |
| print "Exactly one of --revision or --transaction is required" |
| |
| sys.exit(1) |
| |
| # Read options |
| try: |
| (optargs, args) = getopt.getopt(sys.argv[1:], "dt:r:", ["debug", "transaction=", "revision="]) |
| except getopt.GetoptError, e: |
| usage(e) |
| |
| for optarg in optargs: |
| (opt, arg) = optarg |
| if opt in ("-d", "--debug" ): debug += 1 |
| elif opt in ("-t", "--transaction"): transaction = arg |
| elif opt in ("-r", "--revision" ): revision = arg |
| |
| if transaction and revision: |
| usage("Cannot specify both transaction and revision to check") |
| if not transaction and not revision: |
| usage("Must specify transaction or revision to check") |
| |
| if not len(args): usage("No repository") |
| repository = args.pop(0) |
| |
| if not len(args): usage("No config") |
| configuration_filename = args.pop(0) |
| |
| if len(args): usage("Too many arguments") |
| |
| ## |
| # Validation |
| # All rule enforcement goes in these routines |
| ## |
| |
| def open_file(filename): |
| """ |
| Retrieves the contents of the given file. |
| """ |
| cat_cmd = [ "svnlook", "cat", None, repository, filename ] |
| |
| if transaction: cat_cmd[2] = "--transaction=" + transaction |
| elif revision: cat_cmd[2] = "--revision=" + revision |
| else: raise ValueError("No transaction or revision") |
| |
| cat_out, cat_in = popen2.popen2(cat_cmd) |
| cat_in.close() |
| |
| return cat_out |
| |
| def verify_file_added(filename): |
| """ |
| Here we verify file additions which may not meet our requirements. |
| """ |
| if debug: print "Added file %r" % filename |
| if configuration.has_key("verify_file_added"): |
| configuration["verify_file_added"](filename) |
| |
| def verify_file_removed(filename): |
| """ |
| Here we verify file removals which may not meet our requirements. |
| """ |
| if debug: print "Removed file %r" % filename |
| if configuration.has_key("verify_file_removed"): |
| configuration["verify_file_removed"](filename) |
| |
| def verify_file_copied(destination_filename, source_filename): |
| """ |
| Here we verify file copies which may not meet our requirements. |
| """ |
| if debug: print "Copied %r to %r" % (source_filename, destination_filename) |
| if configuration.has_key("verify_file_copied"): |
| configuration["verify_file_copied"](destination_filename, source_filename) |
| |
| def verify_file_modified(filename): |
| """ |
| Here we verify files which may not meet our requirements. |
| Any failure, even if not due to the specific changes in the commit |
| will raise an error. |
| """ |
| if debug: print "Modified file %r" % filename |
| if configuration.has_key("verify_file_modified"): |
| configuration["verify_file_modified"](filename) |
| |
| def verify_line_added(filename, line): |
| """ |
| Here we verify new lines of code which may not meet our requirements. |
| Code not changed as part of this commit is not verified. |
| """ |
| if configuration.has_key("verify_line_added"): |
| configuration["verify_line_added"](filename, line) |
| |
| def verify_line_removed(filename, line): |
| """ |
| Here we verify removed lines of code which may not meet our requirements. |
| Code not changed as part of this commit is not verified. |
| """ |
| if configuration.has_key("verify_line_removed"): |
| configuration["verify_line_removed"](filename, line) |
| |
| def verify_property_line_added(filename, property, line): |
| """ |
| Here we verify added property lines which may not meet our requirements. |
| Code not changed as part of this commit is not verified. |
| """ |
| if debug: print "Add %s::%s: %s" % (filename, property, line) |
| if configuration.has_key("verify_property_line_added"): |
| configuration["verify_property_line_added"](filename, property, line) |
| |
| def verify_property_line_removed(filename, property, line): |
| """ |
| Here we verify removed property lines which may not meet our requirements. |
| Code not changed as part of this commit is not verified. |
| """ |
| if debug: print "Del %s::%s: %s" % (filename, property, line) |
| if configuration.has_key("verify_property_line_removed"): |
| configuration["verify_property_line_removed"](filename, property, line) |
| |
| ## |
| # Do the Right Thing |
| ## |
| |
| configuration = {"open_file": open_file} |
| execfile(configuration_filename, configuration, configuration) |
| |
| diff_cmd = [ "svnlook", "diff", None, repository ] |
| |
| if transaction: diff_cmd[2] = "--transaction=" + transaction |
| elif revision: diff_cmd[2] = "--revision=" + revision |
| else: raise ValueError("No transaction or revision") |
| |
| diff_out, diff_in = popen2.popen2(diff_cmd) |
| diff_in.close() |
| |
| try: |
| state = 0 |
| |
| # |
| # This is the svnlook output parser |
| # |
| for line in diff_out: |
| if line[-1] == "\n": line = line[:-1] # Zap trailing newline |
| |
| # Test cases: |
| # r2266: Added text files, property changes |
| # r18923: Added, deleted, modified text files |
| # r25692: Copied files |
| # r7758: Added binary files |
| |
| if debug > 1: print "%4d: %s" % (state, line) # Useful for testing parser problems |
| |
| if state is -1: # Used for testing new states: print whatever is left |
| print line |
| continue |
| |
| if state in (0, 100, 300): # Initial state or in a state that may return to initial state |
| if state is 0 and not line: continue |
| |
| colon = line.find(":") |
| |
| if state is not 300 and colon != -1 and len(line) > colon + 2: |
| action = line[:colon] |
| filename = line[colon+2:] |
| |
| if action in ( |
| "Modified", |
| "Added", "Deleted", "Copied", |
| "Property changes on", |
| ): |
| if action == "Modified": verify_file_modified(filename) |
| elif action == "Added" : verify_file_added (filename) |
| elif action == "Deleted" : verify_file_removed (filename) |
| elif action == "Copied": |
| i = filename.find(" (from rev ") |
| destination_filename = filename[:i] |
| filename = filename[i:] |
| |
| i = filename.find(", ") |
| assert filename[-1] == ")" |
| source_filename = filename[i+2:-1] |
| |
| verify_file_copied(destination_filename, source_filename) |
| |
| filename = destination_filename |
| |
| if action == "Modified" : state = 10 |
| elif action == "Added" : state = 10 |
| elif action == "Deleted" : state = 10 |
| elif action == "Copied" : state = 20 |
| elif action == "Property changes on": state = 30 |
| else: raise AssertionError("Unknown action") |
| |
| current_filename = filename |
| current_property = None |
| |
| continue |
| |
| assert state in (100, 300) |
| |
| if state is 10: # Expecting a bar (follows "(Added|Modified|Deleted):" line) |
| assert line == "=" * 67 |
| state = 11 |
| continue |
| |
| if state is 11: # Expecting left file info (follows bar) |
| if line == "": state = 0 |
| elif line == "(Binary files differ)": state = 0 |
| elif line.startswith("--- "): state = 12 |
| else: raise AssertionError("Expected left file info, got: %r" % line) |
| |
| continue |
| |
| if state is 12: # Expecting right file info (follows left file info) |
| assert line.startswith("+++ " + current_filename) |
| state = 100 |
| continue |
| |
| if state is 20: # Expecting a bar or blank (follows "Copied:" line) |
| # Test cases: |
| # r25692: Copied and not modified (blank) |
| # r26613: Copied and modified (bar) |
| if not line: |
| state = 0 |
| elif line == "=" * 67: |
| state = 11 |
| else: |
| raise AssertionError("After Copied: line, neither bar nor blank: %r" % line) |
| continue |
| |
| if state is 100: # Expecting diff data |
| for c, verify in (("-", verify_line_removed), ("+", verify_line_added)): |
| if len(line) >= 1 and line[0] == c: |
| try: verify(current_filename, line[1:]) |
| except Exception, e: |
| sys.stderr.write(str(e)) |
| sys.stderr.write("\n") |
| sys.exit(1) |
| break |
| else: |
| if ( |
| not line or |
| (len(line) >= 4 and line[:2] == "@@" == line[-2:]) or |
| (len(line) >= 1 and line[0] == " ") or |
| line == "\\ No newline at end of file" |
| ): |
| continue |
| |
| raise AssertionError("Expected diff data, got: %r" % line) |
| |
| continue |
| |
| if state is 30: # Expecting a bar (follows "Property changes on:" line) |
| assert line == "_" * 67 |
| state = 31 |
| continue |
| |
| if state is 31: # Expecting property name (follows bar) |
| for label in ( |
| "Name", # svn versions < 1.5 |
| "Added", "Modified", "Deleted" # svn versions >= 1.5 |
| ): |
| if line.startswith(label + ": "): |
| break |
| else: |
| raise AssertionError("Unexpected property name line: %r" % line) |
| |
| state = 300 |
| # Fall through to state 300 |
| |
| if state is 300: |
| |
| if not line: |
| state = 0 |
| continue |
| |
| for label in ( |
| "Name", # svn versions < 1.5 |
| "Added", "Modified", "Deleted" # svn versions >= 1.5 |
| ): |
| if line.startswith(label + ": "): |
| current_property = line[len(label)+2:] |
| current_verify_property_function = None |
| break |
| |
| else: |
| for prefix, verify in ( |
| (" - ", verify_property_line_removed), |
| (" + ", verify_property_line_added) |
| ): |
| if line.startswith(prefix): |
| try: verify(current_filename, current_property, line[5:]) |
| except Exception, e: |
| sys.stderr.write(str(e)) |
| sys.stderr.write("\n") |
| sys.exit(1) |
| current_verify_property_function = verify |
| break |
| else: |
| if not line: continue |
| |
| if current_verify_property_function is None: |
| raise AssertionError("Expected property diff data, got: %r" % line) |
| else: |
| # Multi-line property value |
| current_verify_property_function(current_filename, current_property, line) |
| |
| continue |
| |
| raise AssertionError("Unparsed line: %r" % line) |
| |
| if debug: print "Commit is OK" |
| |
| finally: |
| for line in diff_out: pass |
| diff_out.close() |