238 lines
8.1 KiB
Python
238 lines
8.1 KiB
Python
from dataclasses import dataclass
|
|
import argparse
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
activate_print_verbose = False
|
|
|
|
def print_verbose(*args, **kwargs):
|
|
if activate_print_verbose:
|
|
print("VERBOSE:", *args, **kwargs)
|
|
|
|
def print_warning(*args, **kwargs):
|
|
print("WARNING:", *args, **kwargs)
|
|
|
|
def decode_string(string):
|
|
if type(string) is str:
|
|
return string
|
|
|
|
try:
|
|
result = string.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
try:
|
|
result = string.decode("cp1252")
|
|
except UnicodeDecodeError:
|
|
print_warning("Failed to decode so returning original string \"{0}\"".format(string))
|
|
return string
|
|
|
|
return result
|
|
|
|
def run_subprocess(command, **kwargs):
|
|
environment = os.environ.copy()
|
|
|
|
# Add additional paths on the Mac so it can find p4 etc.
|
|
if platform.system() == "Darwin":
|
|
additional_paths = ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin"]
|
|
new_path = os.environ.get("PATH", "")
|
|
for path in additional_paths:
|
|
new_path = os.pathsep.join([new_path, path])
|
|
|
|
environment = environment | {"PATH": new_path}
|
|
|
|
return subprocess.run(command, capture_output=True, env=environment, **kwargs)
|
|
|
|
@dataclass
|
|
class P4_ztag():
|
|
file_path: str = None
|
|
depot_file: str = None
|
|
is_add: bool = False
|
|
is_open: bool = True
|
|
|
|
def p4_ztag(path):
|
|
command = ["p4", "-ztag", "fstat", "-Ro", path]
|
|
result = run_subprocess(command)
|
|
stdout_string = decode_string(result.stdout)
|
|
stderr_string = decode_string(result.stderr)
|
|
|
|
to_return = P4_ztag(file_path = path)
|
|
|
|
if "not opened on this client" in stderr_string:
|
|
to_return.is_open = False
|
|
return to_return
|
|
|
|
lines = stdout_string.splitlines()
|
|
|
|
depot_file_regex = re.compile(r'^\.\.\. depotFile (.*)')
|
|
type_regex = re.compile(r'^\.\.\. type (.*)')
|
|
action_regex = re.compile(r'^\.\.\. action (.*)')
|
|
|
|
for line in lines:
|
|
if match := depot_file_regex.match(line):
|
|
to_return.depot_file = match.group(1)
|
|
if match := type_regex.match(line):
|
|
file_type = match.group(1)
|
|
if file_type.strip() != "text":
|
|
return None
|
|
if match := action_regex.match(line):
|
|
action = match.group(1)
|
|
if action.strip() == "add":
|
|
to_return.is_add = True
|
|
|
|
return to_return
|
|
|
|
def clang_format(exe_path, config_path, file_path, verify=False):
|
|
"""Returns True if the diff needed formatting, False otherwise."""
|
|
|
|
command = [exe_path, "-style=file:%s" % config_path, "-i", file_path]
|
|
if verify:
|
|
command.append("--dry-run")
|
|
command.append("-Werror")
|
|
|
|
result = run_subprocess(command)
|
|
if result.returncode == 1:
|
|
return True
|
|
if result.returncode != 0:
|
|
print("Non-zero return code {0} from {1}".format(result.returncode, " ".join(command)))
|
|
print("stderr:", result.stderr)
|
|
return
|
|
print(decode_string(result.stdout))
|
|
|
|
return False
|
|
|
|
def p4_diff(path):
|
|
command = ["p4", "diff", "-du0", path]
|
|
result = run_subprocess(command)
|
|
if result.returncode != 0:
|
|
raise Exception("p4 diff failed: %s" % result.stderr)
|
|
stdout_string = decode_string(result.stdout)
|
|
return stdout_string
|
|
|
|
def p4_open(path):
|
|
command = ["p4", "open", path]
|
|
result = run_subprocess(command)
|
|
if result.returncode != 0:
|
|
raise Exception("p4 open failed: %s" % result.stderr)
|
|
|
|
def clang_format_diff(clang_format_diff_path, clang_format_path, config_path, diff, verify=False):
|
|
"""Returns True if the diff needed formatting, False otherwise."""
|
|
|
|
# Figure out which python executable to call.
|
|
python_command = None
|
|
if shutil.which("python") is not None:
|
|
python_command = "python"
|
|
elif shutil.which("python3") is not None:
|
|
python_command = "python3"
|
|
else:
|
|
raise Exception("Could not run clang-format-diff.py: could not find \"python\" or \"python3\" executable to run it with.")
|
|
|
|
print_verbose("Using python command \"%s\"." % python_command)
|
|
|
|
command = [python_command, clang_format_diff_path, "-binary=%s" % clang_format_path, "-style=file:%s" % config_path, "-v"]
|
|
|
|
if not verify:
|
|
# Write the formatted code back to the files being formatted.
|
|
command.append("-i")
|
|
|
|
result = run_subprocess(command, text=True, input=diff)
|
|
# clang-format-diff returns 1 when the file needs to be formatted.
|
|
if result.returncode == 1:
|
|
return True
|
|
elif result.returncode != 0:
|
|
print("Non-zero return code {0} from {1}".format(result.returncode, " ".join(command)))
|
|
print("stderr:", result.stderr)
|
|
|
|
return False
|
|
|
|
def main():
|
|
args_parser = argparse.ArgumentParser(
|
|
prog="perforce-clang-format",
|
|
description="Uses Perforce to run clang-format on the diff of files you've changed",
|
|
)
|
|
args_parser.add_argument("paths", nargs="*")
|
|
args_parser.add_argument("-v", "--verbose", action="store_true")
|
|
args_parser.add_argument("--clang-format-path")
|
|
args_parser.add_argument("--clang-format-config-path")
|
|
args_parser.add_argument("--clang-format-diff-path")
|
|
args_parser.add_argument("--verify", action="store_true")
|
|
args = args_parser.parse_args()
|
|
|
|
global activate_print_verbose
|
|
activate_print_verbose = args.verbose
|
|
|
|
print_verbose("args.verify=", args.verify)
|
|
print_verbose("args.paths=", args.paths)
|
|
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
clang_format_path = args.clang_format_path
|
|
if clang_format_path is None:
|
|
if platform.system() == "Darwin":
|
|
clang_format_path = os.path.join(script_dir, "Mac-arm64", "clang-format")
|
|
elif platform.system() == "Windows":
|
|
clang_format_path = os.path.join(script_dir, "Win64", "clang-format.exe")
|
|
elif platform.system() == "Linux":
|
|
raise Exception("Linux is not yet supported.")
|
|
else:
|
|
# Support the old clang-format.exe location for now.
|
|
clang_format_path = os.path.join(script_dir, "clang-format.exe")
|
|
|
|
config_path = args.clang_format_config_path
|
|
if config_path is None:
|
|
config_path = os.path.join(script_dir, "experimental.clang-format")
|
|
|
|
clang_format_diff_path = args.clang_format_diff_path
|
|
if clang_format_diff_path is None:
|
|
clang_format_diff_path = os.path.join(script_dir, "clang-format-diff.py")
|
|
|
|
files_that_need_formatting = []
|
|
|
|
for path in args.paths:
|
|
ztag = p4_ztag(path)
|
|
if ztag is None:
|
|
continue
|
|
|
|
if ztag.is_add:
|
|
if args.verify:
|
|
print_verbose("Checking formatting of new file \"%s\"." % ztag.file_path)
|
|
else:
|
|
print_verbose("Fully formatting new file \"%s\"." % ztag.file_path)
|
|
|
|
if clang_format(clang_format_path, config_path, ztag.file_path, args.verify):
|
|
if args.verify:
|
|
files_that_need_formatting.append(ztag.file_path)
|
|
|
|
elif not ztag.is_open:
|
|
if not args.verify:
|
|
print_verbose("Opening unopened file \"%s\"." % ztag.file_path)
|
|
p4_open(ztag.file_path)
|
|
diff = p4_diff(ztag.file_path)
|
|
|
|
if args.verify:
|
|
print_verbose("Checking formatting of diff of file \"%s\"." % ztag.file_path)
|
|
else:
|
|
print_verbose("Formatting diff of file \"%s\"." % ztag.file_path)
|
|
|
|
if clang_format_diff(clang_format_diff_path, clang_format_path, config_path, diff, args.verify):
|
|
if args.verify:
|
|
files_that_need_formatting.append(ztag.file_path)
|
|
|
|
else:
|
|
diff = p4_diff(ztag.file_path)
|
|
print_verbose("Formatting diff of file \"%s\"." % ztag.file_path)
|
|
|
|
if clang_format_diff(clang_format_diff_path, clang_format_path, config_path, diff, args.verify):
|
|
if args.verify:
|
|
files_that_need_formatting.append(ztag.file_path)
|
|
|
|
if len(files_that_need_formatting) > 0:
|
|
for file_path in files_that_need_formatting:
|
|
print("File needs formatting: \"%s\"." % file_path)
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|