1#!/usr/bin/env python3 2# 3# === clang-format-diff.py - ClangFormat Diff Reformatter ---*- python -*-=== # 4# 5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6# See https://llvm.org/LICENSE.txt for license information. 7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8# 9# ===---------------------------------------------------------------------=== # 10 11""" 12This script reads input from a unified diff and reformats all the changed 13lines. This is useful to reformat all the lines touched by a specific patch. 14Example usage for git/svn users: 15 16 git diff -U0 --no-color HEAD^ | clang-format-diff.py -p1 -i 17 svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i 18 19""" 20from __future__ import absolute_import, division, print_function 21 22import argparse 23import difflib 24import re 25import subprocess 26import sys 27 28if sys.version_info.major >= 3: 29 from io import StringIO 30else: 31 from io import BytesIO as StringIO 32 33 34def main(): 35 parser = argparse.ArgumentParser( 36 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter 37 ) 38 parser.add_argument( 39 "-i", 40 action="store_true", 41 default=False, 42 help="apply edits to files instead of displaying a " "diff", 43 ) 44 parser.add_argument( 45 "-p", 46 metavar="NUM", 47 default=0, 48 help="strip the smallest prefix containing P slashes", 49 ) 50 parser.add_argument( 51 "-regex", 52 metavar="PATTERN", 53 default=None, 54 help="custom pattern selecting file paths to reformat " 55 "(case sensitive, overrides -iregex)", 56 ) 57 parser.add_argument( 58 "-iregex", 59 metavar="PATTERN", 60 default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hh|hpp|m|mm|inc" 61 r"|js|ts|proto|protodevel|java|cs)", 62 help="custom pattern selecting file paths to reformat " 63 "(case insensitive, overridden by -regex)", 64 ) 65 parser.add_argument( 66 "-sort-includes", 67 action="store_true", 68 default=False, 69 help="let clang-format sort include blocks", 70 ) 71 parser.add_argument( 72 "-v", 73 "--verbose", 74 action="store_true", 75 help="be more verbose, ineffective without -i", 76 ) 77 parser.add_argument( 78 "-style", 79 help="formatting style to apply (LLVM, Google, " "Chromium, Mozilla, WebKit)", 80 ) 81 parser.add_argument( 82 "-binary", 83 default="clang-format", 84 help="location of binary to use for clang-format", 85 ) 86 args = parser.parse_args() 87 88 # Extract changed lines for each file. 89 filename = None 90 lines_by_file = {} 91 for line in sys.stdin: 92 match = re.search(r"^\+\+\+\ (.*?/){%s}(\S*)" % args.p, line) 93 if match: 94 filename = match.group(2) 95 if filename is None: 96 continue 97 98 if args.regex is not None: 99 if not re.match("^%s$" % args.regex, filename): 100 continue 101 else: 102 if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE): 103 continue 104 105 match = re.search(r"^@@.*\+(\d+)(,(\d+))?", line) 106 if match: 107 start_line = int(match.group(1)) 108 line_count = 1 109 if match.group(3): 110 line_count = int(match.group(3)) 111 if line_count == 0: 112 continue 113 end_line = start_line + line_count - 1 114 lines_by_file.setdefault(filename, []).extend( 115 ["-lines", str(start_line) + ":" + str(end_line)] 116 ) 117 118 # Reformat files containing changes in place. 119 # We need to count amount of bytes generated in the output of 120 # clang-format-diff. If clang-format-diff doesn't generate any bytes it 121 # means there is nothing to format. 122 format_line_counter = 0 123 for filename, lines in lines_by_file.items(): 124 if args.i and args.verbose: 125 print("Formatting {}".format(filename)) 126 command = [args.binary, filename] 127 if args.i: 128 command.append("-i") 129 if args.sort_includes: 130 command.append("-sort-includes") 131 command.extend(lines) 132 if args.style: 133 command.extend(["-style", args.style]) 134 p = subprocess.Popen( 135 command, 136 stdout=subprocess.PIPE, 137 stderr=None, 138 stdin=subprocess.PIPE, 139 universal_newlines=True, 140 ) 141 stdout, _ = p.communicate() 142 if p.returncode != 0: 143 sys.exit(p.returncode) 144 145 if not args.i: 146 with open(filename) as f: 147 code = f.readlines() 148 formatted_code = StringIO(stdout).readlines() 149 diff = difflib.unified_diff( 150 code, 151 formatted_code, 152 filename, 153 filename, 154 "(before formatting)", 155 "(after formatting)", 156 ) 157 diff_string = "".join(diff) 158 if diff_string: 159 format_line_counter += sys.stdout.write(diff_string) 160 161 if format_line_counter > 0: 162 sys.exit(1) 163 164 165if __name__ == "__main__": 166 main() 167