1*e038c9c4Sjoerg#!/usr/bin/env python3 27330f729Sjoerg# 37330f729Sjoerg#===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===# 47330f729Sjoerg# 57330f729Sjoerg# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 67330f729Sjoerg# See https://llvm.org/LICENSE.txt for license information. 77330f729Sjoerg# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 87330f729Sjoerg# 97330f729Sjoerg#===------------------------------------------------------------------------===# 107330f729Sjoerg 117330f729Sjoerg""" 127330f729SjoergThis script reads input from a unified diff and reformats all the changed 137330f729Sjoerglines. This is useful to reformat all the lines touched by a specific patch. 147330f729SjoergExample usage for git/svn users: 157330f729Sjoerg 16*e038c9c4Sjoerg git diff -U0 --no-color --relative HEAD^ | clang-format-diff.py -p1 -i 177330f729Sjoerg svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i 187330f729Sjoerg 19*e038c9c4SjoergIt should be noted that the filename contained in the diff is used unmodified 20*e038c9c4Sjoergto determine the source file to update. Users calling this script directly 21*e038c9c4Sjoergshould be careful to ensure that the path in the diff is correct relative to the 22*e038c9c4Sjoergcurrent working directory. 237330f729Sjoerg""" 247330f729Sjoergfrom __future__ import absolute_import, division, print_function 257330f729Sjoerg 267330f729Sjoergimport argparse 277330f729Sjoergimport difflib 287330f729Sjoergimport re 297330f729Sjoergimport subprocess 307330f729Sjoergimport sys 317330f729Sjoerg 327330f729Sjoergif sys.version_info.major >= 3: 337330f729Sjoerg from io import StringIO 347330f729Sjoergelse: 357330f729Sjoerg from io import BytesIO as StringIO 367330f729Sjoerg 377330f729Sjoerg 387330f729Sjoergdef main(): 397330f729Sjoerg parser = argparse.ArgumentParser(description=__doc__, 407330f729Sjoerg formatter_class= 417330f729Sjoerg argparse.RawDescriptionHelpFormatter) 427330f729Sjoerg parser.add_argument('-i', action='store_true', default=False, 437330f729Sjoerg help='apply edits to files instead of displaying a diff') 447330f729Sjoerg parser.add_argument('-p', metavar='NUM', default=0, 457330f729Sjoerg help='strip the smallest prefix containing P slashes') 467330f729Sjoerg parser.add_argument('-regex', metavar='PATTERN', default=None, 477330f729Sjoerg help='custom pattern selecting file paths to reformat ' 487330f729Sjoerg '(case sensitive, overrides -iregex)') 497330f729Sjoerg parser.add_argument('-iregex', metavar='PATTERN', default= 50*e038c9c4Sjoerg r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hh|hpp|hxx|m|mm|inc|js|ts' 51*e038c9c4Sjoerg r'|proto|protodevel|java|cs)', 527330f729Sjoerg help='custom pattern selecting file paths to reformat ' 537330f729Sjoerg '(case insensitive, overridden by -regex)') 547330f729Sjoerg parser.add_argument('-sort-includes', action='store_true', default=False, 557330f729Sjoerg help='let clang-format sort include blocks') 567330f729Sjoerg parser.add_argument('-v', '--verbose', action='store_true', 577330f729Sjoerg help='be more verbose, ineffective without -i') 587330f729Sjoerg parser.add_argument('-style', 59*e038c9c4Sjoerg help='formatting style to apply (LLVM, GNU, Google, Chromium, ' 60*e038c9c4Sjoerg 'Microsoft, Mozilla, WebKit)') 617330f729Sjoerg parser.add_argument('-binary', default='clang-format', 627330f729Sjoerg help='location of binary to use for clang-format') 637330f729Sjoerg args = parser.parse_args() 647330f729Sjoerg 657330f729Sjoerg # Extract changed lines for each file. 667330f729Sjoerg filename = None 677330f729Sjoerg lines_by_file = {} 687330f729Sjoerg for line in sys.stdin: 697330f729Sjoerg match = re.search(r'^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line) 707330f729Sjoerg if match: 717330f729Sjoerg filename = match.group(2) 72*e038c9c4Sjoerg if filename is None: 737330f729Sjoerg continue 747330f729Sjoerg 757330f729Sjoerg if args.regex is not None: 767330f729Sjoerg if not re.match('^%s$' % args.regex, filename): 777330f729Sjoerg continue 787330f729Sjoerg else: 797330f729Sjoerg if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 807330f729Sjoerg continue 817330f729Sjoerg 827330f729Sjoerg match = re.search(r'^@@.*\+(\d+)(,(\d+))?', line) 837330f729Sjoerg if match: 847330f729Sjoerg start_line = int(match.group(1)) 857330f729Sjoerg line_count = 1 867330f729Sjoerg if match.group(3): 877330f729Sjoerg line_count = int(match.group(3)) 887330f729Sjoerg if line_count == 0: 897330f729Sjoerg continue 907330f729Sjoerg end_line = start_line + line_count - 1 917330f729Sjoerg lines_by_file.setdefault(filename, []).extend( 927330f729Sjoerg ['-lines', str(start_line) + ':' + str(end_line)]) 937330f729Sjoerg 947330f729Sjoerg # Reformat files containing changes in place. 957330f729Sjoerg for filename, lines in lines_by_file.items(): 967330f729Sjoerg if args.i and args.verbose: 977330f729Sjoerg print('Formatting {}'.format(filename)) 987330f729Sjoerg command = [args.binary, filename] 997330f729Sjoerg if args.i: 1007330f729Sjoerg command.append('-i') 1017330f729Sjoerg if args.sort_includes: 1027330f729Sjoerg command.append('-sort-includes') 1037330f729Sjoerg command.extend(lines) 1047330f729Sjoerg if args.style: 1057330f729Sjoerg command.extend(['-style', args.style]) 106*e038c9c4Sjoerg 107*e038c9c4Sjoerg try: 1087330f729Sjoerg p = subprocess.Popen(command, 1097330f729Sjoerg stdout=subprocess.PIPE, 1107330f729Sjoerg stderr=None, 1117330f729Sjoerg stdin=subprocess.PIPE, 1127330f729Sjoerg universal_newlines=True) 113*e038c9c4Sjoerg except OSError as e: 114*e038c9c4Sjoerg # Give the user more context when clang-format isn't 115*e038c9c4Sjoerg # found/isn't executable, etc. 116*e038c9c4Sjoerg raise RuntimeError( 117*e038c9c4Sjoerg 'Failed to run "%s" - %s"' % (" ".join(command), e.strerror)) 118*e038c9c4Sjoerg 1197330f729Sjoerg stdout, stderr = p.communicate() 1207330f729Sjoerg if p.returncode != 0: 1217330f729Sjoerg sys.exit(p.returncode) 1227330f729Sjoerg 1237330f729Sjoerg if not args.i: 1247330f729Sjoerg with open(filename) as f: 1257330f729Sjoerg code = f.readlines() 1267330f729Sjoerg formatted_code = StringIO(stdout).readlines() 1277330f729Sjoerg diff = difflib.unified_diff(code, formatted_code, 1287330f729Sjoerg filename, filename, 1297330f729Sjoerg '(before formatting)', '(after formatting)') 1307330f729Sjoerg diff_string = ''.join(diff) 1317330f729Sjoerg if len(diff_string) > 0: 1327330f729Sjoerg sys.stdout.write(diff_string) 1337330f729Sjoerg 1347330f729Sjoergif __name__ == '__main__': 1357330f729Sjoerg main() 136