| #!/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() |