Functionality:
Every time developer commit java source code to repository (svn), pre-commit hook will call PMD http://pmd.sourceforge.net/. If there will be violations - error with detailed description will be generated, and developer will be stopped from committing bad code.
Solution is based on standard svn example for validate-files. I did PoC on windows machine - so I have to spend some additional time to fight with .bat->.py integration, but mac/linux should me much more easier /straight forward solution.
Prerequisites are: svn + java + python + pmd.
Installation steps:
1) Create your SVN repository:
svnadmin create c:\svnrepo
2) Copy into hooks directory: (c:\svnrepo\hooks)
2.a) pre-commit.py
#!/usr/bin/env python """Subversion pre-commit hook script that runs PMD static code analysis. Functionality: Runs PMD checks on java source code. Commit will be rejected if PMD rules are voilated. If there are more than 40 files committed at once - commit will be rejected. Don't kill SVN server - commit in smaller chunks. To avoid PMD checks - put NOPMD into SVN log. The script expects a pmd-check.conf file placed in the conf dir under the repo the commit is for.""" import sys import os import subprocess import fnmatch import tempfile # Deal with the rename of ConfigParser to configparser in Python3 try: # Python >= 3.0 import configparser except ImportError: # Python < 3.0 import ConfigParser as configparser class Commands: """Class to handle logic of running commands""" def __init__(self, config): self.config = config def svnlook_changed(self, repo, txn): """Provide list of files changed in txn of repo""" svnlook = self.config.get('DEFAULT', 'svnlook') cmd = "%s changed -t %s %s" % (svnlook, txn, repo) # sys.stderr.write("Command:: %s\n" % cmd) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) lines = (line.strip() for line in p.stdout) # Only if the contents of the file changed (by addition or update) # directories always end in / in the svnlook changed output changed = [line[4:] for line in lines if line[-1] != "/" and line[0] in ("A","U") ] # wait on the command to finish so we can get the # returncode/stderr output data = p.communicate() if p.returncode != 0: sys.stderr.write(data[1].decode()) sys.exit(2) return changed def svnlook_getlog(self, repo, txn): """ Gets content of svn log""" svnlook = self.config.get('DEFAULT', 'svnlook') cmd = "%s log -t %s %s" % (svnlook, txn, repo) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) data = p.communicate() return (p.returncode, data[0].decode()) def svnlook_getfile(self, repo, txn, fn, tmp): """ Gets content of svn file""" svnlook = self.config.get('DEFAULT', 'svnlook') cmd = "%s cat -t %s %s %s > %s" % (svnlook, txn, repo, fn, tmp) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) data = p.communicate() return (p.returncode, data[1].decode()) def pmd_command(self, repo, txn, fn, tmp): """ Run the PMD scan over created temporary java file""" pmd = self.config.get('DEFAULT', 'pmd') pmd_rules = self.config.get('DEFAULT', 'pmd_rules') cmd = "%s -f text -R %s -d %s" % (pmd, pmd_rules, tmp) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) data = p.communicate() # pmd is not working on error codes .. return (p.returncode, data[0].decode()) def main(repo, txn): exitcode = 0 config = configparser.SafeConfigParser() config.read(os.path.join(repo, 'conf', 'pmd-check.conf')) commands = Commands(config) # check if someone put magic string to not process code with PMD (returncode, log) = commands.svnlook_getlog(repo, txn) if returncode != 0: sys.stderr.write( "\nError retrieving log from svn " \ "(exit code %d):\n" % (returncode)) sys.exit(returncode); if "NOPMD" in log: sys.stderr.write("No PMD check - mail should be sent instead.") sys.exit(0) # get list of changed files during this commit changed = commands.svnlook_changed(repo, txn) # this happens when you adding new project to repo if len(changed) == 0: sys.stderr.write("No files changed in SVN!!!\n") sys.exit(0) # we don't want to kill svn server or wait hours for commit if len(fnmatch.filter(changed, "*.java")) >= 40: sys.stderr.write( "Too many files to process, try commiting " \ " less than 40 java files per session \n" \ " Or put 'NOPMD' in comment, if you need " \ " to work with bigger chunks!\n") sys.exit(1) # create temporary file f = tempfile.NamedTemporaryFile(suffix='.java',prefix='x',delete=False) f.close() # only java files for fn in fnmatch.filter(changed, "*.java"): (returncode, err_mesg) = commands.svnlook_getfile( repo, txn, fn, f.name) if returncode != 0: sys.stderr.write( "\nError retrieving file '%s' from svn " \ "(exit code %d):\n" % (fn, returncode)) sys.stderr.write(err_mesg) (returncode, err_mesg) = commands.pmd_command( repo, txn, fn, f.name) if returncode != 0: sys.stderr.write( "\nError validating file '%s'" \ "(exit code %d):\n" % (fn, returncode)) sys.stderr.write(err_mesg) exitcode = 1 if len(err_mesg) != 0: sys.stderr.write( "\nPMD violations in file '%s' \n" % fn) sys.stderr.write(err_mesg) exitcode = 1 os.remove(f.name) return exitcode if __name__ == "__main__": if len(sys.argv) != 3: sys.stderr.write("invalid args\n") sys.exit(1) try: sys.exit(main(sys.argv[1], sys.argv[2])) except configparser.Error as e: sys.stderr.write("Error with the pmd-check.conf: %s\n" % e) sys.exit(1)
2.b) pre-commit.bat [ if this is windows ]
c:/python27/python pre_commit.py %1 %2 exit %ERRORLEVEL%;
you can change paths to python/repository
3) Copy pmd-check.conf into conf directory (c:\svnrepo\conf)
[DEFAULT] svnlook = c:\\opt\\svn\\svnlook.exe pmd = c:\\opt\\pmd\\bin\\pmd.bat pmd_rules = java-basic,java-braces,java-clone,java-codesize,java-comments,java-design,java-empty,java-finalizers,java-imports,java-j2ee,java-javabeans,java-junit,java-logging-java,java-naming,java-optimizations,java-strictexception,java-strings,java-typeresolution,java-unnecessary,java-unusedcode #not in scope #java-controversial, java-coupling, java-android, java-javabeans, java-logging-jakarta-commons, java-sunsecure #java-migrating, java-migrating_to_13, java-migrating_to_14, java-migrating_to_15, java-migrating_to_junit14And this is file that you should change depending on your svn/pmd installation paths and what PMD rules you want to check.
Final tip: To avoid PMD check on top of standard PMD suppressers - you can just put "NOPMD" into comment.
Have fun!
/Jaro
This is my very first python stuff - so please by lenient... however if you'll found some improvements - share!
ReplyDeleteArtur - Thanks for review - code updated with your suggestions!
ReplyDeleteAnd yes I like iPython!
This is working fine in windows. But not in unix.
ReplyDeletei am not able to run this, can you please assist me,
Deletei am working on tortoiseHG in Windows OS
if __name__ == "__main__":
ReplyDeleteif len(sys.argv) != 3:
sys.stderr.write("invalid args\n")
sys.exit(1)
try:
sys.exit(main(sys.argv[1], sys.argv[2]))
except configparser.Error as e:
sys.stderr.write("Error with the pmd-check.conf: %s\n" % e)
sys.exit(1)
This section does not work for me. Throwing some error on the py file.Please suggest me
I am able to make it run in my local win7 desktop but not able to use proper PMD rule/ruleset XML.
ReplyDeletePlease suggest how to use ruleset XML?
Please help me for the above problem
ReplyDelete