1a9ac8606Spatrick#!/usr/bin/env python3 2e5dd7070Spatrick# 3e5dd7070Spatrick#===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===# 4e5dd7070Spatrick# 5e5dd7070Spatrick# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6e5dd7070Spatrick# See https://llvm.org/LICENSE.txt for license information. 7e5dd7070Spatrick# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8e5dd7070Spatrick# 9e5dd7070Spatrick#===------------------------------------------------------------------------===# 10e5dd7070Spatrick 11e5dd7070Spatrick""" 12e5dd7070SpatrickThis script reads input from a unified diff and reformats all the changed 13e5dd7070Spatricklines. This is useful to reformat all the lines touched by a specific patch. 14e5dd7070SpatrickExample usage for git/svn users: 15e5dd7070Spatrick 16ec727ea7Spatrick git diff -U0 --no-color --relative HEAD^ | clang-format-diff.py -p1 -i 17e5dd7070Spatrick svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i 18e5dd7070Spatrick 19ec727ea7SpatrickIt should be noted that the filename contained in the diff is used unmodified 20ec727ea7Spatrickto determine the source file to update. Users calling this script directly 21ec727ea7Spatrickshould be careful to ensure that the path in the diff is correct relative to the 22ec727ea7Spatrickcurrent working directory. 23e5dd7070Spatrick""" 24e5dd7070Spatrickfrom __future__ import absolute_import, division, print_function 25e5dd7070Spatrick 26e5dd7070Spatrickimport argparse 27e5dd7070Spatrickimport difflib 28e5dd7070Spatrickimport re 29e5dd7070Spatrickimport subprocess 30e5dd7070Spatrickimport sys 31e5dd7070Spatrick 32e5dd7070Spatrickif sys.version_info.major >= 3: 33e5dd7070Spatrick from io import StringIO 34e5dd7070Spatrickelse: 35e5dd7070Spatrick from io import BytesIO as StringIO 36e5dd7070Spatrick 37e5dd7070Spatrick 38e5dd7070Spatrickdef main(): 39e5dd7070Spatrick parser = argparse.ArgumentParser(description=__doc__, 40e5dd7070Spatrick formatter_class= 41e5dd7070Spatrick argparse.RawDescriptionHelpFormatter) 42e5dd7070Spatrick parser.add_argument('-i', action='store_true', default=False, 43e5dd7070Spatrick help='apply edits to files instead of displaying a diff') 44e5dd7070Spatrick parser.add_argument('-p', metavar='NUM', default=0, 45e5dd7070Spatrick help='strip the smallest prefix containing P slashes') 46e5dd7070Spatrick parser.add_argument('-regex', metavar='PATTERN', default=None, 47e5dd7070Spatrick help='custom pattern selecting file paths to reformat ' 48e5dd7070Spatrick '(case sensitive, overrides -iregex)') 49e5dd7070Spatrick parser.add_argument('-iregex', metavar='PATTERN', default= 50*12c85518Srobert r'.*\.(cpp|cc|c\+\+|cxx|cppm|ccm|cxxm|c\+\+m|c|cl|h|hh|hpp|hxx' 51*12c85518Srobert r'|m|mm|inc|js|ts|proto|protodevel|java|cs|json)', 52e5dd7070Spatrick help='custom pattern selecting file paths to reformat ' 53e5dd7070Spatrick '(case insensitive, overridden by -regex)') 54e5dd7070Spatrick parser.add_argument('-sort-includes', action='store_true', default=False, 55e5dd7070Spatrick help='let clang-format sort include blocks') 56e5dd7070Spatrick parser.add_argument('-v', '--verbose', action='store_true', 57e5dd7070Spatrick help='be more verbose, ineffective without -i') 58e5dd7070Spatrick parser.add_argument('-style', 59ec727ea7Spatrick help='formatting style to apply (LLVM, GNU, Google, Chromium, ' 60ec727ea7Spatrick 'Microsoft, Mozilla, WebKit)') 61*12c85518Srobert parser.add_argument('-fallback-style', 62*12c85518Srobert help='The name of the predefined style used as a' 63*12c85518Srobert 'fallback in case clang-format is invoked with' 64*12c85518Srobert '-style=file, but can not find the .clang-format' 65*12c85518Srobert 'file to use.') 66e5dd7070Spatrick parser.add_argument('-binary', default='clang-format', 67e5dd7070Spatrick help='location of binary to use for clang-format') 68e5dd7070Spatrick args = parser.parse_args() 69e5dd7070Spatrick 70e5dd7070Spatrick # Extract changed lines for each file. 71e5dd7070Spatrick filename = None 72e5dd7070Spatrick lines_by_file = {} 73e5dd7070Spatrick for line in sys.stdin: 74e5dd7070Spatrick match = re.search(r'^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line) 75e5dd7070Spatrick if match: 76e5dd7070Spatrick filename = match.group(2) 77ec727ea7Spatrick if filename is None: 78e5dd7070Spatrick continue 79e5dd7070Spatrick 80e5dd7070Spatrick if args.regex is not None: 81e5dd7070Spatrick if not re.match('^%s$' % args.regex, filename): 82e5dd7070Spatrick continue 83e5dd7070Spatrick else: 84e5dd7070Spatrick if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 85e5dd7070Spatrick continue 86e5dd7070Spatrick 87e5dd7070Spatrick match = re.search(r'^@@.*\+(\d+)(,(\d+))?', line) 88e5dd7070Spatrick if match: 89e5dd7070Spatrick start_line = int(match.group(1)) 90e5dd7070Spatrick line_count = 1 91e5dd7070Spatrick if match.group(3): 92e5dd7070Spatrick line_count = int(match.group(3)) 93*12c85518Srobert # Also format lines range if line_count is 0 in case of deleting 94*12c85518Srobert # surrounding statements. 95*12c85518Srobert end_line = start_line 96*12c85518Srobert if line_count != 0: 97*12c85518Srobert end_line += line_count - 1 98e5dd7070Spatrick lines_by_file.setdefault(filename, []).extend( 99e5dd7070Spatrick ['-lines', str(start_line) + ':' + str(end_line)]) 100e5dd7070Spatrick 101e5dd7070Spatrick # Reformat files containing changes in place. 102e5dd7070Spatrick for filename, lines in lines_by_file.items(): 103e5dd7070Spatrick if args.i and args.verbose: 104e5dd7070Spatrick print('Formatting {}'.format(filename)) 105e5dd7070Spatrick command = [args.binary, filename] 106e5dd7070Spatrick if args.i: 107e5dd7070Spatrick command.append('-i') 108e5dd7070Spatrick if args.sort_includes: 109e5dd7070Spatrick command.append('-sort-includes') 110e5dd7070Spatrick command.extend(lines) 111e5dd7070Spatrick if args.style: 112e5dd7070Spatrick command.extend(['-style', args.style]) 113*12c85518Srobert if args.fallback_style: 114*12c85518Srobert command.extend(['-fallback-style', args.fallback_style]) 115a9ac8606Spatrick 116a9ac8606Spatrick try: 117e5dd7070Spatrick p = subprocess.Popen(command, 118e5dd7070Spatrick stdout=subprocess.PIPE, 119e5dd7070Spatrick stderr=None, 120e5dd7070Spatrick stdin=subprocess.PIPE, 121e5dd7070Spatrick universal_newlines=True) 122a9ac8606Spatrick except OSError as e: 123a9ac8606Spatrick # Give the user more context when clang-format isn't 124a9ac8606Spatrick # found/isn't executable, etc. 125a9ac8606Spatrick raise RuntimeError( 126a9ac8606Spatrick 'Failed to run "%s" - %s"' % (" ".join(command), e.strerror)) 127a9ac8606Spatrick 128e5dd7070Spatrick stdout, stderr = p.communicate() 129e5dd7070Spatrick if p.returncode != 0: 130e5dd7070Spatrick sys.exit(p.returncode) 131e5dd7070Spatrick 132e5dd7070Spatrick if not args.i: 133e5dd7070Spatrick with open(filename) as f: 134e5dd7070Spatrick code = f.readlines() 135e5dd7070Spatrick formatted_code = StringIO(stdout).readlines() 136e5dd7070Spatrick diff = difflib.unified_diff(code, formatted_code, 137e5dd7070Spatrick filename, filename, 138e5dd7070Spatrick '(before formatting)', '(after formatting)') 139e5dd7070Spatrick diff_string = ''.join(diff) 140e5dd7070Spatrick if len(diff_string) > 0: 141e5dd7070Spatrick sys.stdout.write(diff_string) 142e5dd7070Spatrick 143e5dd7070Spatrickif __name__ == '__main__': 144e5dd7070Spatrick main() 145