Skip to content

Instantly share code, notes, and snippets.

@cmcginty
Last active September 21, 2021 23:12
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cmcginty/8d99e0eac345e06dc1ba to your computer and use it in GitHub Desktop.
Save cmcginty/8d99e0eac345e06dc1ba to your computer and use it in GitHub Desktop.
Perforce Python trigger to reject unmodified files and new code with TAB indents (instead of spaces).

This is a working example of a Perforce submit-change trigger that can run checks on new files and new or modified lines of code.

Tested on Python2 in Linux and Windows. It does not require the P4Python API, which would probably be a little cleaner, but makes it less portable.

The install instructions are below in the Python file. Before using, the hard-coded P4 user value builds must be created on your server, or changed to a valid user. To prevent having to set the user password in the script, the builds user must be part of a group with the Timeout field set to unlimited to allow for the user to remain logged in on the server.

#!/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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment