blob: 0fda38d6eb8056053686f21ee227fcfe08bee9b0 [file] [log] [blame]
#!/usr/bin/env python3
#
#===- format_diff.py - Diff Reformatter ----*- python3 -*--===#
#
# This file is licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
#===------------------------------------------------------------------------===#
"""
This script reads input from a unified diff and reformats all the changed
lines. This is useful to reformat all the lines touched by a specific patch.
Example usage:
git diff -U0 HEAD^ | python3 format_diff.py yapf -i
git diff -U0 HEAD^ | python3 format_diff.py clang-format -i
svn diff --diff-cmd=diff -x-U0 | python3 format_diff.py -p0 clang-format -i
General usage:
<some diff> | python3 format_diff.py [--regex] [--lines-style] [-p] binary [args for binary]
It should be noted that the filename contained in the diff is used unmodified
to determine the source file to update. Users calling this script directly
should be careful to ensure that the path in the diff is correct relative to the
current working directory.
"""
import argparse
import difflib
import io
import re
import subprocess
import sys
BINARY_TO_DEFAULT_REGEX = {
"yapf": r".*\.py",
"clang-format":
r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hh|hpp|hxx|m|mm|inc|js|ts|proto|"
r"protodevel|java|cs)",
}
def parse_arguments():
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
"binary",
help="Location of binary to use for formatting. This controls the "
"default values of --regex and --lines-style. If binary isn't 'yapf' "
"or 'clang-format' then --regex and --lines-style are required.")
parser.add_argument(
"--regex",
metavar="PATTERN",
default=None,
help="Regex pattern for selecting file paths to reformat from the piped "
"diff. This flag is required if 'binary' is not set to 'yapf' or "
"'clang-format'. Otherwise, this flag overrides the default pattern that "
"--binary sets.")
parser.add_argument(
"--lines-style",
default=None,
help="How to style the 'lines' argument for the given binary. Can be set "
"to 'yapf' or 'clang-format'. This flag is required if 'binary' is not "
"set to 'yapf' or 'clang-format'.")
parser.add_argument(
"-p",
metavar="NUM",
default=1,
help="Strip the smallest prefix containing P slashes. Set to 0 if "
"passing `--no-prefix` to `git diff` or using `svn`")
# Parse and error-check arguments
args, binary_args = parser.parse_known_args()
if args.binary not in BINARY_TO_DEFAULT_REGEX:
if not args.regex:
raise parser.error("If 'binary' is not 'yapf' or 'clang-format' then "
"--regex must be set.")
if not args.lines_style:
raise parser.error("If 'binary' is not 'yapf' or 'clang-format' then "
"--lines-style must be set.")
else:
# Set defaults based off of 'binary'.
if not args.regex:
args.regex = BINARY_TO_DEFAULT_REGEX[args.binary]
if not args.lines_style:
args.lines_style = args.binary
if args.lines_style not in ["yapf", "clang-format"]:
raise parser.error(f"Unexpected value for --line-style {args.lines_style}")
return args, binary_args
def main():
args, binary_args = parse_arguments()
# Extract changed lines for each file.
filename = None
lines_by_file = {}
for line in sys.stdin:
# Match all filenames.
match = re.search(fr"^\+\+\+\ (.*?/){{{args.p}}}(\S*)", line)
if match:
filename = match.group(2)
if filename is None:
continue
# Match all filenames specified by --regex.
if not re.match(f"^{args.regex}$", filename):
continue
# Match unified diff line numbers.
match = re.search(r"^@@.*\+(\d+)(,(\d+))?", line)
if match:
start_line = int(match.group(1))
line_count = 1
if match.group(3):
line_count = int(match.group(3))
if line_count == 0:
continue
end_line = start_line + line_count - 1
if args.lines_style == "yapf":
lines = ["--lines", f"{start_line}-{end_line}"]
elif args.lines_style == "clang-format":
lines = ["-lines", f"{start_line}:{end_line}"]
lines_by_file.setdefault(filename, []).extend(lines)
# Pass the changed lines to 'binary' alongside any unparsed args (e.g. -i).
for filename, lines in lines_by_file.items():
command = [args.binary, filename]
command.extend(lines)
command.extend(binary_args)
print(f"Running `{' '.join(command)}`")
p = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=None,
stdin=subprocess.PIPE,
universal_newlines=True)
stdout, stderr = p.communicate()
if p.returncode != 0:
sys.exit(p.returncode)
# If the formatter printed the formatted code to stdout then print out
# a unified diff between the formatted and unformatted code.
# If flags like --verbose are passed to the binary then the diffs this
# produces won't be particularly helpful.
formatted_code = io.StringIO(stdout).readlines()
if len(formatted_code):
with open(filename) as f:
unformatted_code = f.readlines()
diff = difflib.unified_diff(unformatted_code,
formatted_code,
fromfile=filename,
tofile=filename,
fromfiledate="(before formatting)",
tofiledate="(after formatting)")
diff_string = "".join(diff)
if len(diff_string) > 0:
sys.stdout.write(diff_string)
if __name__ == "__main__":
main()