#!/usr/bin/env python3 # # === clang-format-diff.py - ClangFormat Diff Reformatter ---*- python -*-=== # # # Part of the LLVM Project, 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 for git/svn users: git diff -U0 --no-color HEAD^ | clang-format-diff.py -p1 -i svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i """ from __future__ import absolute_import, division, print_function import argparse import difflib import re import subprocess import sys if sys.version_info.major >= 3: from io import StringIO else: from io import BytesIO as StringIO def main(): parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( "-i", action="store_true", default=False, help="apply edits to files instead of displaying a " "diff", ) parser.add_argument( "-p", metavar="NUM", default=0, help="strip the smallest prefix containing P slashes", ) parser.add_argument( "-regex", metavar="PATTERN", default=None, help="custom pattern selecting file paths to reformat " "(case sensitive, overrides -iregex)", ) parser.add_argument( "-iregex", metavar="PATTERN", default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hh|hpp|m|mm|inc" r"|js|ts|proto|protodevel|java|cs)", help="custom pattern selecting file paths to reformat " "(case insensitive, overridden by -regex)", ) parser.add_argument( "-sort-includes", action="store_true", default=False, help="let clang-format sort include blocks", ) parser.add_argument( "-v", "--verbose", action="store_true", help="be more verbose, ineffective without -i", ) parser.add_argument( "-style", help="formatting style to apply (LLVM, Google, " "Chromium, Mozilla, WebKit)", ) parser.add_argument( "-binary", default="clang-format", help="location of binary to use for clang-format", ) args = parser.parse_args() # Extract changed lines for each file. filename = None lines_by_file = {} for line in sys.stdin: match = re.search(r"^\+\+\+\ (.*?/){%s}(\S*)" % args.p, line) if match: filename = match.group(2) if filename is None: continue if args.regex is not None: if not re.match("^%s$" % args.regex, filename): continue else: if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE): continue 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 lines_by_file.setdefault(filename, []).extend( ["-lines", str(start_line) + ":" + str(end_line)] ) # Reformat files containing changes in place. # We need to count amount of bytes generated in the output of # clang-format-diff. If clang-format-diff doesn't generate any bytes it # means there is nothing to format. format_line_counter = 0 for filename, lines in lines_by_file.items(): if args.i and args.verbose: print("Formatting {}".format(filename)) command = [args.binary, filename] if args.i: command.append("-i") if args.sort_includes: command.append("-sort-includes") command.extend(lines) if args.style: command.extend(["-style", args.style]) p = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=None, stdin=subprocess.PIPE, universal_newlines=True, ) stdout, _ = p.communicate() if p.returncode != 0: sys.exit(p.returncode) if not args.i: with open(filename) as f: code = f.readlines() formatted_code = StringIO(stdout).readlines() diff = difflib.unified_diff( code, formatted_code, filename, filename, "(before formatting)", "(after formatting)", ) diff_string = "".join(diff) if diff_string: format_line_counter += sys.stdout.write(diff_string) if format_line_counter > 0: sys.exit(1) if __name__ == "__main__": main()