Add support for running Checkstyle on a given SHA-1.
Usage e.g.: ./checkstyle.py -s 3aca02ca91816a86febb5ed6e380ec2122adea47 Additionally this CL adds a pre-push script that can be used a git hook to run Checkstyle on 'repo upload' Bug: 25852971 Change-Id: Ia00d48df80b2b024de0899d9590868016c5b7bf0
This commit is contained in:
@@ -21,8 +21,10 @@
|
||||
import argparse
|
||||
import errno
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import xml.dom.minidom
|
||||
import gitlint.git as git
|
||||
|
||||
@@ -35,46 +37,94 @@ FORCED_RULES = ['com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck
|
||||
ERROR_UNCOMMITTED = 'You need to commit all modified files before running Checkstyle\n'
|
||||
ERROR_UNTRACKED = 'You have untracked java files that are not being checked:\n'
|
||||
|
||||
def RunCheckstyle(java_files):
|
||||
if not os.path.exists(CHECKSTYLE_STYLE):
|
||||
print 'Java checkstyle configuration file is missing'
|
||||
sys.exit(1)
|
||||
|
||||
if java_files:
|
||||
# Files to check were specified via command line.
|
||||
print 'Running Checkstyle on inputted files'
|
||||
sha = ''
|
||||
last_commit_modified_files = {}
|
||||
java_files = map(os.path.abspath, java_files)
|
||||
else:
|
||||
# Files where not specified exclicitly. Let's use last commit files.
|
||||
sha, last_commit_modified_files = _GetModifiedFiles()
|
||||
print 'Running Checkstyle on %s commit' % sha
|
||||
java_files = last_commit_modified_files.keys()
|
||||
def RunCheckstyleOnFiles(java_files):
|
||||
"""Runs Checkstyle checks on a given set of java_files.
|
||||
|
||||
if not java_files:
|
||||
print 'No files to check'
|
||||
return
|
||||
Args:
|
||||
java_files: A list of files to check.
|
||||
|
||||
Returns:
|
||||
A tuple of errors and warnings.
|
||||
"""
|
||||
print 'Running Checkstyle on inputted files'
|
||||
java_files = map(os.path.abspath, java_files)
|
||||
stdout = _ExecuteCheckstyle(java_files)
|
||||
(errors, warnings) = _ParseAndFilterOutput(stdout,
|
||||
sha,
|
||||
last_commit_modified_files)
|
||||
(errors, warnings) = _ParseAndFilterOutput(stdout)
|
||||
_PrintErrorsAndWarnings(errors, warnings)
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def RunCheckstyleOnACommit(commit):
|
||||
"""Runs Checkstyle checks on a given commit.
|
||||
|
||||
It will run Checkstyle on the changed Java files in a specified commit SHA-1
|
||||
and if that is None it will fallback to check the latest commit of the
|
||||
currently checked out branch.
|
||||
|
||||
Args:
|
||||
commit: A full 40 character SHA-1 of a commit to check.
|
||||
|
||||
Returns:
|
||||
A tuple of errors and warnings.
|
||||
"""
|
||||
if not commit:
|
||||
_WarnIfUntrackedFiles()
|
||||
commit = git.last_commit()
|
||||
print 'Running Checkstyle on %s commit' % commit
|
||||
commit_modified_files = _GetModifiedFiles(commit)
|
||||
if not commit_modified_files.keys():
|
||||
print 'No Java files to check'
|
||||
return [], []
|
||||
|
||||
(tmp_dir, tmp_file_map) = _GetTempFilesForCommit(
|
||||
commit_modified_files.keys(), commit)
|
||||
|
||||
java_files = tmp_file_map.keys()
|
||||
stdout = _ExecuteCheckstyle(java_files)
|
||||
|
||||
# Remove all the temporary files.
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
(errors, warnings) = _ParseAndFilterOutput(stdout,
|
||||
commit,
|
||||
commit_modified_files,
|
||||
tmp_file_map)
|
||||
_PrintErrorsAndWarnings(errors, warnings)
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def _WarnIfUntrackedFiles(out=sys.stdout):
|
||||
"""Prints a warning and a list of untracked files if needed."""
|
||||
root = git.repository_root()
|
||||
untracked_files = git.modified_files(root, False)
|
||||
untracked_files = {f for f in untracked_files if f.endswith('.java')}
|
||||
if untracked_files:
|
||||
out.write(ERROR_UNTRACKED)
|
||||
for untracked_file in untracked_files:
|
||||
out.write(untracked_file + '\n')
|
||||
out.write('\n')
|
||||
|
||||
|
||||
def _PrintErrorsAndWarnings(errors, warnings):
|
||||
"""Prints given errors and warnings."""
|
||||
if errors:
|
||||
print 'ERRORS:'
|
||||
print '\n'.join(errors)
|
||||
if warnings:
|
||||
print 'WARNINGS:'
|
||||
print '\n'.join(warnings)
|
||||
if errors or warnings:
|
||||
sys.exit(1)
|
||||
|
||||
print 'SUCCESS! NO ISSUES FOUND'
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def _ExecuteCheckstyle(java_files):
|
||||
"""Runs Checkstyle to check give Java files for style errors.
|
||||
|
||||
Args:
|
||||
java_files: A list of Java files that needs to be checked.
|
||||
|
||||
Returns:
|
||||
Checkstyle output in XML format.
|
||||
"""
|
||||
# Run checkstyle
|
||||
checkstyle_env = os.environ.copy()
|
||||
checkstyle_env['JAVA_CMD'] = 'java'
|
||||
@@ -90,32 +140,38 @@ def _ExecuteCheckstyle(java_files):
|
||||
print 'Error running Checkstyle!'
|
||||
sys.exit(1)
|
||||
|
||||
# Work-around for Checkstyle printing error count to stdio.
|
||||
# A work-around for Checkstyle printing error count to stdio.
|
||||
if 'Checkstyle ends with' in stdout.splitlines()[-1]:
|
||||
stdout = '\n'.join(stdout.splitlines()[:-1])
|
||||
return stdout
|
||||
|
||||
def _ParseAndFilterOutput(stdout, sha, last_commit_modified_files):
|
||||
|
||||
def _ParseAndFilterOutput(stdout,
|
||||
sha=None,
|
||||
commit_modified_files=None,
|
||||
tmp_file_map=None):
|
||||
result_errors = []
|
||||
result_warnings = []
|
||||
root = xml.dom.minidom.parseString(stdout)
|
||||
for file_element in root.getElementsByTagName('file'):
|
||||
file_name = file_element.attributes['name'].value
|
||||
if last_commit_modified_files:
|
||||
if tmp_file_map:
|
||||
file_name = tmp_file_map[file_name]
|
||||
if commit_modified_files:
|
||||
modified_lines = git.modified_lines(file_name,
|
||||
last_commit_modified_files[file_name],
|
||||
commit_modified_files[file_name],
|
||||
sha)
|
||||
file_name = os.path.relpath(file_name)
|
||||
errors = file_element.getElementsByTagName('error')
|
||||
for error in errors:
|
||||
line = int(error.attributes['line'].value)
|
||||
rule = error.attributes['source'].value
|
||||
if last_commit_modified_files and _ShouldSkip(modified_lines, line, rule):
|
||||
continue
|
||||
if commit_modified_files and _ShouldSkip(modified_lines, line, rule):
|
||||
continue
|
||||
|
||||
column = ''
|
||||
if error.hasAttribute('column'):
|
||||
column = '%s:' % (error.attributes['column'].value)
|
||||
column = '%s:' % error.attributes['column'].value
|
||||
message = error.attributes['message'].value
|
||||
result = ' %s:%s:%s %s' % (file_name, line, column, message)
|
||||
|
||||
@@ -124,44 +180,99 @@ def _ParseAndFilterOutput(stdout, sha, last_commit_modified_files):
|
||||
result_errors.append(result)
|
||||
elif severity == 'warning':
|
||||
result_warnings.append(result)
|
||||
return (result_errors, result_warnings)
|
||||
return result_errors, result_warnings
|
||||
|
||||
|
||||
# Returns whether an error on a given line should be skipped
|
||||
# based on the modified_lines list and the rule.
|
||||
def _ShouldSkip(modified_lines, line, rule):
|
||||
"""Returns whether an error on a given line should be skipped.
|
||||
|
||||
Args:
|
||||
modified_lines: A list of lines that has been modified.
|
||||
line: The line that has a rule violation.
|
||||
rule: The type of rule that a given line is violating.
|
||||
|
||||
Returns:
|
||||
A boolean whether a given line should be skipped in the reporting.
|
||||
"""
|
||||
# None modified_lines means checked file is new and nothing should be skipped.
|
||||
if modified_lines is None:
|
||||
return False
|
||||
return line not in modified_lines and rule not in FORCED_RULES
|
||||
|
||||
|
||||
def _GetModifiedFiles(out = sys.stdout):
|
||||
def _GetModifiedFiles(commit, out=sys.stdout):
|
||||
root = git.repository_root()
|
||||
sha = git.last_commit()
|
||||
pending_files = git.modified_files(root, True)
|
||||
if pending_files:
|
||||
out.write(ERROR_UNCOMMITTED)
|
||||
sys.exit(1)
|
||||
untracked_files = git.modified_files(root, False)
|
||||
untracked_files = {f for f in untracked_files if f.endswith('.java')}
|
||||
if untracked_files:
|
||||
out.write(ERROR_UNTRACKED)
|
||||
for untracked_file in untracked_files:
|
||||
out.write(untracked_file + '\n')
|
||||
out.write('\n')
|
||||
|
||||
modified_files = git.modified_files(root, True, sha)
|
||||
modified_files = git.modified_files(root, True, commit)
|
||||
modified_files = {f: modified_files[f] for f
|
||||
in modified_files if f.endswith('.java')}
|
||||
return (sha, modified_files)
|
||||
return modified_files
|
||||
|
||||
|
||||
def _GetTempFilesForCommit(file_names, commit):
|
||||
"""Creates a temporary snapshot of the files in at a commit.
|
||||
|
||||
Retrieves the state of every file in file_names at a given commit and writes
|
||||
them all out to a temporary directory.
|
||||
|
||||
Args:
|
||||
file_names: A list of files that need to be retrieved.
|
||||
commit: A full 40 character SHA-1 of a commit.
|
||||
|
||||
Returns:
|
||||
A tuple of temprorary directory name and a directionary of
|
||||
temp_file_name: filename. For example:
|
||||
|
||||
('/tmp/random/', {'/tmp/random/blarg.java': 'real/path/to/file.java' }
|
||||
"""
|
||||
tmp_dir_name = tempfile.mkdtemp()
|
||||
tmp_file_names = {}
|
||||
for file_name in file_names:
|
||||
content = subprocess.check_output(
|
||||
['git', 'show', commit + ':' + os.path.relpath(file_name)])
|
||||
(fd, tmp_file_name) = tempfile.mkstemp(suffix='.java',
|
||||
dir=tmp_dir_name,
|
||||
text=True)
|
||||
tmp_file = os.fdopen(fd, 'w')
|
||||
tmp_file.write(content)
|
||||
tmp_file.close()
|
||||
tmp_file_names[tmp_file_name] = file_name
|
||||
return tmp_dir_name, tmp_file_names
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Runs Checkstyle checks on a given set of java files or a commit.
|
||||
|
||||
It will run Checkstyle on the list of java files first, if unspecified,
|
||||
then the check will be run on a specified commit SHA-1 and if that
|
||||
is None it will fallback to check the latest commit of the currently checked
|
||||
out branch.
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--file', '-f', nargs='+')
|
||||
parser.add_argument('--sha', '-s')
|
||||
args = parser.parse_args()
|
||||
RunCheckstyle(args.file)
|
||||
|
||||
if not os.path.exists(CHECKSTYLE_STYLE):
|
||||
print 'Java checkstyle configuration file is missing'
|
||||
sys.exit(1)
|
||||
|
||||
if args.file:
|
||||
# Files to check were specified via command line.
|
||||
(errors, warnings) = RunCheckstyleOnFiles(args.file)
|
||||
else:
|
||||
(errors, warnings) = RunCheckstyleOnACommit(args.sha)
|
||||
|
||||
if errors or warnings:
|
||||
sys.exit(1)
|
||||
|
||||
print 'SUCCESS! NO ISSUES FOUND'
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user