|
#!/usr/bin/env python2 |
|
""" |
|
This command is a Perforce trigger that will run validation checks on the code |
|
before accepting it into the database. The checks that are implemented include: |
|
|
|
Tab Indents - fail source code indented with tab characters |
|
|
|
To install in perforce, you must create a new trigger with 'p4 triggers' |
|
command. Then add the line: |
|
|
|
StyleCheck change-content //depot/... "D:\Python27\python.exe D:\p4-trigger-style-check.py %changelist%" |
|
|
|
The script runs as the 'builds' user and this user must logged in on the |
|
Perforce server: |
|
|
|
p4 -u builds login |
|
""" |
|
|
|
# python 3 compatibility |
|
from __future__ import absolute_import |
|
from __future__ import division |
|
from __future__ import print_function |
|
from __future__ import unicode_literals |
|
|
|
from itertools import imap, repeat |
|
import argparse |
|
import logging |
|
import os |
|
import pprint |
|
import re |
|
import subprocess |
|
import sys |
|
|
|
# only file extensions in this list will be processed |
|
FILE_EXTENSION_WHITELIST = ('.bat', |
|
'.conf', |
|
'.css', |
|
'.gradle', |
|
'.java', |
|
'.js', |
|
'.properties', |
|
'.ps1', |
|
'.py',) |
|
|
|
|
|
def init_args(): |
|
"""Parse input arguments and place into global ARGS var.""" |
|
global ARGS |
|
parser = argparse.ArgumentParser() |
|
parser.add_argument('changelist', |
|
help="submitted or pending changelist to check") |
|
parser.add_argument('--debug', |
|
help="enable debug logging", |
|
action='store_true') |
|
parser.add_argument('--trigger', |
|
help="set option in trigger command only", |
|
action='store_true') |
|
ARGS = parser.parse_args() |
|
if ARGS.debug: |
|
logging.basicConfig(level=logging.DEBUG) |
|
# set the default P4 user |
|
os.environ['P4USER'] = 'builds' |
|
|
|
|
|
def p4_diff2(file_): |
|
"""Return output lines of p4 diff2 on specificed file and global changelist.""" |
|
if not ARGS.trigger: |
|
# this mode is used for debugging the script with an old CL |
|
cmd = ['p4', 'diff2', '{0}@{1}'.format(file_, int(ARGS.changelist) - 1), |
|
'{0}@{1}'.format(file_, ARGS.changelist)] # diff submitted CL |
|
else: |
|
cmd = ['p4', 'diff2', |
|
'{0}@{1}'.format(file_, get_last_submitted_changelist()), |
|
'{0}@={1}'.format(file_, ARGS.changelist)] # diff pending CL |
|
logging.debug(cmd) |
|
return subprocess.check_output(cmd) |
|
|
|
|
|
def get_last_submitted_changelist(): |
|
"""Query to get last submitted changelist number.""" |
|
cmd = ['p4', 'changes', '-m', '1', '-s', 'submitted'] |
|
logging.debug(cmd) |
|
lines = subprocess.check_output(cmd) |
|
cl = re.match(r'Change ([0-9]+) .*', lines).group(1) |
|
logging.debug(cl) |
|
return cl |
|
|
|
|
|
def whitelisted_files_in_changelist(): |
|
"""Return a list of tuples with of each whitelisted 'file path' and |
|
'revision' in the changelist.""" |
|
return filter( |
|
lambda (f, rev, mode): os.path.splitext(f)[1] in FILE_EXTENSION_WHITELIST, |
|
files_in_changelist()) |
|
|
|
|
|
def files_in_changelist(): |
|
"""Return a list of tuples of each 'file path' and 'revision' in the changelist.""" |
|
|
|
cmd = ['p4', 'files', '@={0}'.format(ARGS.changelist)] |
|
logging.debug(cmd) |
|
lines = subprocess.check_output(cmd) |
|
p4_file_path = re.compile(r'(.*)#(\d+) - (\w+).*') |
|
file_revs = [m.groups() |
|
for m in [p4_file_path.match(l) for l in lines.splitlines()] |
|
if m] |
|
logging.debug(pprint.pformat(file_revs)) |
|
return file_revs |
|
|
|
|
|
def find_unmodified_file_edits(file_revs): |
|
"""Return True when an 'edit' file is found with no changes.""" |
|
identical = re.compile(r'==== .* ==== identical') |
|
for f in [f for (f, rev, mode) in file_revs if mode == 'edit']: |
|
found_match = any(identical.match(l) for l in p4_diff2(f).splitlines()) |
|
if found_match: |
|
print("ERROR: No changes in '{0}'. " |
|
"Remove or edit file to submit change.".format(f)) |
|
return True |
|
return False |
|
|
|
|
|
def new_lines_in_changelist(file_revs): |
|
"""Return list of added lines in changelist from source files.""" |
|
new_lines = [] |
|
for f, rev, mode in file_revs: |
|
# use file revision to determine if this is an 'edit' or an 'add' |
|
if int(rev) > 1: |
|
new_lines += new_lines_in_file_edit(f) |
|
else: |
|
assert (rev == '1') |
|
new_lines += all_lines_in_file_rev1(f) |
|
logging.debug(pprint.pformat(new_lines)) |
|
return new_lines |
|
|
|
|
|
def new_lines_in_file_edit(file_): |
|
"""Return all new lines added in current changelist on input Perforce file""" |
|
lines = p4_diff2(file_) |
|
p4_diff2_new_line = re.compile(r'^>\ (.*)') |
|
return [m.group(1) |
|
for m in [p4_diff2_new_line.match(l) for l in lines.splitlines()] |
|
if m] |
|
|
|
def all_lines_in_file_rev1(file_): |
|
"""Return all lines from #1 (rev1) version of input Perforce file.""" |
|
|
|
cmd = ['p4', 'print', '{0}#1'.format(file_)] |
|
logging.debug(cmd) |
|
return subprocess.check_output(cmd).splitlines() |
|
|
|
|
|
def execute_line_check_functions(lines): |
|
"""Run all defined tests on every new source line. |
|
Return True when any test is True.""" |
|
return any(imap(lambda f, x: f(x), LINE_CHECK_FUNCTS, repeat(lines))) |
|
|
|
|
|
def find_tab_indents(lines): |
|
"""Return TRUE if a TAB character is found.""" |
|
leading_tab = re.compile(r'^[\s]*[\t]') |
|
found_match = any([leading_tab.match(l) for l in lines]) |
|
logging.debug("SYNTAX: Leading TAB character found: {0}".format( |
|
found_match)) |
|
if found_match: |
|
print( |
|
"ERROR: Changelist contains TAB indents. " |
|
"https://google.github.io/styleguide/javaguide.html#s4.2-block-indentation") |
|
return found_match |
|
|
|
# Global list of functions called to check source file changes. Check functions |
|
# must return True when an error condition is detected. |
|
LINE_CHECK_FUNCTS = [find_tab_indents] |
|
|
|
if __name__ == '__main__': |
|
init_args() |
|
all_file_revs = files_in_changelist() |
|
not find_unmodified_file_edits(all_file_revs) or sys.exit(1) |
|
|
|
src_file_revs = whitelisted_files_in_changelist() |
|
lines = new_lines_in_changelist(src_file_revs) |
|
not execute_line_check_functions(lines) or sys.exit(1) |