utils: Add Python-based commit style checker script
checkstyle.py is a reimplementation of checkstyle.sh in Python, that should be easier to extend with additional features. Three additional features and enhancements are already implemented: - While retaining the default behaviour of operating on the HEAD commit, a list of commits can also be specified on the command line. - Correct line numbers are printed in the diff output. - The index and working tree are not touched, they can be dirty. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
This commit is contained in:
parent
cebe684c19
commit
8b30bb3185
1 changed files with 267 additions and 0 deletions
267
utils/checkstyle.py
Executable file
267
utils/checkstyle.py
Executable file
|
@ -0,0 +1,267 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright (C) 2018, Google Inc.
|
||||||
|
#
|
||||||
|
# Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
|
||||||
|
#
|
||||||
|
# checkstyle.py - A patch style checker script based on astyle
|
||||||
|
#
|
||||||
|
# TODO:
|
||||||
|
#
|
||||||
|
# - Support other formatting tools (clang-format, ...)
|
||||||
|
# - Split large hunks to minimize context noise
|
||||||
|
# - Improve style issues counting
|
||||||
|
#
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import difflib
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
astyle_options = (
|
||||||
|
'-n',
|
||||||
|
'--style=linux',
|
||||||
|
'--indent=force-tab=8',
|
||||||
|
'--attach-namespaces',
|
||||||
|
'--attach-extern-c',
|
||||||
|
'--pad-oper',
|
||||||
|
'--align-pointer=name',
|
||||||
|
'--align-reference=name',
|
||||||
|
'--max-code-length=120'
|
||||||
|
)
|
||||||
|
|
||||||
|
source_extensions = (
|
||||||
|
'.c',
|
||||||
|
'.cpp',
|
||||||
|
'.h'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Colours:
|
||||||
|
Default = 0
|
||||||
|
Red = 31
|
||||||
|
Green = 32
|
||||||
|
Cyan = 36
|
||||||
|
|
||||||
|
for attr in Colours.__dict__.keys():
|
||||||
|
if attr.startswith('_'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
setattr(Colours, attr, '\033[0;%um' % getattr(Colours, attr))
|
||||||
|
else:
|
||||||
|
setattr(Colours, attr, '')
|
||||||
|
|
||||||
|
|
||||||
|
class DiffHunkSide(object):
|
||||||
|
"""A side of a diff hunk, recording line numbers"""
|
||||||
|
def __init__(self, start):
|
||||||
|
self.start = start
|
||||||
|
self.touched = []
|
||||||
|
self.untouched = []
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.touched) + len(self.untouched)
|
||||||
|
|
||||||
|
|
||||||
|
class DiffHunk(object):
|
||||||
|
diff_header_regex = re.compile('@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@')
|
||||||
|
|
||||||
|
def __init__(self, line):
|
||||||
|
match = DiffHunk.diff_header_regex.match(line)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError("Malformed diff hunk header '%s'" % line)
|
||||||
|
|
||||||
|
self.__from_line = int(match.group(1))
|
||||||
|
self.__to_line = int(match.group(3))
|
||||||
|
self.__from = DiffHunkSide(self.__from_line)
|
||||||
|
self.__to = DiffHunkSide(self.__to_line)
|
||||||
|
|
||||||
|
self.lines = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
s = '%s@@ -%u,%u +%u,%u @@\n' % \
|
||||||
|
(Colours.Cyan,
|
||||||
|
self.__from.start, len(self.__from),
|
||||||
|
self.__to.start, len(self.__to))
|
||||||
|
|
||||||
|
for line in self.lines:
|
||||||
|
if line[0] == '-':
|
||||||
|
s += Colours.Red
|
||||||
|
elif line[0] == '+':
|
||||||
|
s += Colours.Green
|
||||||
|
else:
|
||||||
|
s += Colours.Default
|
||||||
|
s += line
|
||||||
|
|
||||||
|
s += Colours.Default
|
||||||
|
return s
|
||||||
|
|
||||||
|
def append(self, line):
|
||||||
|
if line[0] == ' ':
|
||||||
|
self.__from.untouched.append(self.__from_line)
|
||||||
|
self.__from_line += 1
|
||||||
|
self.__to.untouched.append(self.__to_line)
|
||||||
|
self.__to_line += 1
|
||||||
|
elif line[0] == '-':
|
||||||
|
self.__from.touched.append(self.__from_line)
|
||||||
|
self.__from_line += 1
|
||||||
|
elif line[0] == '+':
|
||||||
|
self.__to.touched.append(self.__to_line)
|
||||||
|
self.__to_line += 1
|
||||||
|
|
||||||
|
self.lines.append(line)
|
||||||
|
|
||||||
|
def intersects(self, lines):
|
||||||
|
for line in lines:
|
||||||
|
if line in self.__from.touched:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def side(self, side):
|
||||||
|
if side == 'from':
|
||||||
|
return self.__from
|
||||||
|
else:
|
||||||
|
return self.__to
|
||||||
|
|
||||||
|
|
||||||
|
def parse_diff(diff):
|
||||||
|
hunks = []
|
||||||
|
hunk = None
|
||||||
|
for line in diff:
|
||||||
|
if line.startswith('@@'):
|
||||||
|
if hunk:
|
||||||
|
hunks.append(hunk)
|
||||||
|
hunk = DiffHunk(line)
|
||||||
|
|
||||||
|
elif hunk is not None:
|
||||||
|
hunk.append(line)
|
||||||
|
|
||||||
|
if hunk:
|
||||||
|
hunks.append(hunk)
|
||||||
|
|
||||||
|
return hunks
|
||||||
|
|
||||||
|
|
||||||
|
def check_file(commit, filename):
|
||||||
|
# Extract the line numbers touched by the commit.
|
||||||
|
diff = subprocess.run(['git', 'diff', '%s~..%s' % (commit, commit), '--', filename],
|
||||||
|
stdout=subprocess.PIPE).stdout
|
||||||
|
diff = diff.decode('utf-8').splitlines(True)
|
||||||
|
commit_diff = parse_diff(diff)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for hunk in commit_diff:
|
||||||
|
lines.extend(hunk.side('to').touched)
|
||||||
|
|
||||||
|
# Skip commits that don't add any line.
|
||||||
|
if len(lines) == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Format the file after the commit with astyle and compute the diff between
|
||||||
|
# the two files.
|
||||||
|
after = subprocess.run(['git', 'show', '%s:%s' % (commit, filename)],
|
||||||
|
stdout=subprocess.PIPE).stdout
|
||||||
|
formatted = subprocess.run(['astyle', *astyle_options],
|
||||||
|
input=after, stdout=subprocess.PIPE).stdout
|
||||||
|
|
||||||
|
after = after.decode('utf-8').splitlines(True)
|
||||||
|
formatted = formatted.decode('utf-8').splitlines(True)
|
||||||
|
|
||||||
|
diff = difflib.unified_diff(after, formatted)
|
||||||
|
|
||||||
|
# Split the diff in hunks, recording line number ranges for each hunk.
|
||||||
|
formatted_diff = parse_diff(diff)
|
||||||
|
|
||||||
|
# Filter out hunks that are not touched by the commit.
|
||||||
|
formatted_diff = [hunk for hunk in formatted_diff if hunk.intersects(lines)]
|
||||||
|
if len(formatted_diff) == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print('%s---' % Colours.Red, filename)
|
||||||
|
print('%s+++' % Colours.Green, filename)
|
||||||
|
for hunk in formatted_diff:
|
||||||
|
print(hunk)
|
||||||
|
|
||||||
|
return len(formatted_diff)
|
||||||
|
|
||||||
|
|
||||||
|
def check_style(commit):
|
||||||
|
# Get the commit title and list of files.
|
||||||
|
ret = subprocess.run(['git', 'show', '--pretty=oneline','--name-only', commit],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
files = ret.stdout.decode('utf-8').splitlines()
|
||||||
|
title = files[0]
|
||||||
|
files = files[1:]
|
||||||
|
|
||||||
|
separator = '-' * len(title)
|
||||||
|
print(separator)
|
||||||
|
print(title)
|
||||||
|
print(separator)
|
||||||
|
|
||||||
|
# Filter out non C/C++ files.
|
||||||
|
files = [f for f in files if f.endswith(source_extensions)]
|
||||||
|
if len(files) == 0:
|
||||||
|
print("Commit doesn't touch source files, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
issues = 0
|
||||||
|
for f in files:
|
||||||
|
issues += check_file(commit, f)
|
||||||
|
|
||||||
|
if issues == 0:
|
||||||
|
print("No style issue detected")
|
||||||
|
else:
|
||||||
|
print('---')
|
||||||
|
print("%u potential style %s detected, please review" % \
|
||||||
|
(issues, 'issue' if issues == 1 else 'issues'))
|
||||||
|
|
||||||
|
|
||||||
|
def extract_revlist(revs):
|
||||||
|
"""Extract a list of commits on which to operate from a revision or revision
|
||||||
|
range.
|
||||||
|
"""
|
||||||
|
ret = subprocess.run(['git', 'rev-parse', revs], stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
if ret.returncode != 0:
|
||||||
|
print(ret.stderr.decode('utf-8').splitlines()[0])
|
||||||
|
return []
|
||||||
|
|
||||||
|
revlist = ret.stdout.decode('utf-8').splitlines()
|
||||||
|
|
||||||
|
# If the revlist contains more than one item, pass it to git rev-list to list
|
||||||
|
# each commit individually.
|
||||||
|
if len(revlist) > 1:
|
||||||
|
ret = subprocess.run(['git', 'rev-list', *revlist], stdout=subprocess.PIPE)
|
||||||
|
revlist = ret.stdout.decode('utf-8').splitlines()
|
||||||
|
revlist.reverse()
|
||||||
|
|
||||||
|
return revlist
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('revision_range', type=str, default='HEAD', nargs='?',
|
||||||
|
help='Revision range (as defined by git rev-parse). Defaults to HEAD if not specified.')
|
||||||
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
|
# Check for required dependencies.
|
||||||
|
dependencies = ('astyle', 'git')
|
||||||
|
|
||||||
|
for dependency in dependencies:
|
||||||
|
if not shutil.which(dependency):
|
||||||
|
print("Executable %s not found" % dependency)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
revlist = extract_revlist(args.revision_range)
|
||||||
|
|
||||||
|
for commit in revlist:
|
||||||
|
check_style(commit)
|
||||||
|
print('')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(sys.argv))
|
Loading…
Add table
Add a link
Reference in a new issue