1#!/usr/bin/env python3 2# 3# ===- google-java-format-diff.py - google-java-format Diff Reformatter -----===# 4# 5# The LLVM Compiler Infrastructure 6# 7# This file is distributed under the University of Illinois Open Source 8# License. See LICENSE.TXT for details. 9# 10# ===------------------------------------------------------------------------===# 11 12""" 13google-java-format Diff Reformatter 14============================ 15 16This script reads input from a unified diff and reformats all the changed 17lines. This is useful to reformat all the lines touched by a specific patch. 18Example usage for git/svn users: 19 20 git diff -U0 HEAD^ | google-java-format-diff.py -p1 -i 21 svn diff --diff-cmd=diff -x-U0 | google-java-format-diff.py -i 22 23For perforce users: 24 25 P4DIFF="git --no-pager diff --no-index" p4 diff | ./google-java-format-diff.py -i -p7 26 27""" 28 29import argparse 30import difflib 31import re 32import string 33import subprocess 34import io 35import sys 36from concurrent.futures import ThreadPoolExecutor,wait,FIRST_EXCEPTION 37from shutil import which 38 39def _apply_format(filename, lines, base_command, args): 40 """Apply format on filename.""" 41 if args.i and args.verbose: 42 print('Formatting', filename) 43 44 command = base_command[:] 45 command.extend(lines) 46 command.append(filename) 47 p = subprocess.Popen(command, stdout=subprocess.PIPE, 48 stderr=None, stdin=subprocess.PIPE) 49 stdout, _ = p.communicate() 50 if p.returncode != 0: 51 sys.exit(p.returncode) 52 53 if not args.i: 54 with open(filename) as f: 55 code = f.readlines() 56 formatted_code = io.StringIO(stdout.decode('utf-8')).readlines() 57 diff = difflib.unified_diff(code, formatted_code, 58 filename, filename, 59 '(before formatting)', '(after formatting)') 60 diff_string = ''.join(diff) 61 if len(diff_string) > 0: 62 sys.stdout.write(diff_string) 63 64def main(): 65 parser = argparse.ArgumentParser(description= 66 'Reformat changed lines in diff. Without -i ' 67 'option just output the diff that would be ' 68 'introduced.') 69 parser.add_argument('-i', action='store_true', default=False, 70 help='apply edits to files instead of displaying a diff') 71 72 parser.add_argument('-p', metavar='NUM', default=0, 73 help='strip the smallest prefix containing P slashes') 74 parser.add_argument('-regex', metavar='PATTERN', default=None, 75 help='custom pattern selecting file paths to reformat ' 76 '(case sensitive, overrides -iregex)') 77 parser.add_argument('-iregex', metavar='PATTERN', default=r'.*\.java', 78 help='custom pattern selecting file paths to reformat ' 79 '(case insensitive, overridden by -regex)') 80 parser.add_argument('-v', '--verbose', action='store_true', 81 help='be more verbose, ineffective without -i') 82 parser.add_argument('-a', '--aosp', action='store_true', 83 help='use AOSP style instead of Google Style (4-space indentation)') 84 parser.add_argument('--skip-sorting-imports', action='store_true', 85 help='do not fix the import order') 86 parser.add_argument('--skip-removing-unused-imports', action='store_true', 87 help='do not remove ununsed imports') 88 parser.add_argument( 89 '--skip-javadoc-formatting', 90 action='store_true', 91 default=False, 92 help='do not reformat javadoc') 93 parser.add_argument('-b', '--binary', help='path to google-java-format binary') 94 parser.add_argument('--google-java-format-jar', metavar='ABSOLUTE_PATH', default=None, 95 help='use a custom google-java-format jar') 96 97 args = parser.parse_args() 98 99 # Extract changed lines for each file. 100 filename = None 101 lines_by_file = {} 102 103 for line in sys.stdin: 104 match = re.search(r'^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line) 105 if match: 106 filename = match.group(2) 107 if filename == None: 108 continue 109 110 if args.regex is not None: 111 if not re.match('^%s$' % args.regex, filename): 112 continue 113 else: 114 if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 115 continue 116 117 match = re.search(r'^@@.*\+(\d+)(,(\d+))?', line) 118 if match: 119 start_line = int(match.group(1)) 120 line_count = 1 121 if match.group(3): 122 line_count = int(match.group(3)) 123 if line_count == 0: 124 continue 125 end_line = start_line + line_count - 1; 126 lines_by_file.setdefault(filename, []).extend( 127 ['-lines', str(start_line) + ':' + str(end_line)]) 128 129 if args.binary: 130 base_command = [args.binary] 131 elif args.google_java_format_jar: 132 base_command = ['java', '-jar', args.google_java_format_jar] 133 else: 134 binary = which('google-java-format') or '/usr/bin/google-java-format' 135 base_command = [binary] 136 137 if args.i: 138 base_command.append('-i') 139 if args.aosp: 140 base_command.append('--aosp') 141 if args.skip_sorting_imports: 142 base_command.append('--skip-sorting-imports') 143 if args.skip_removing_unused_imports: 144 base_command.append('--skip-removing-unused-imports') 145 if args.skip_javadoc_formatting: 146 base_command.append('--skip-javadoc-formatting') 147 148 with ThreadPoolExecutor() as executor: 149 format_futures = [] 150 for filename, lines in lines_by_file.items(): 151 format_futures.append( 152 executor.submit(_apply_format, filename, lines, base_command, args) 153 ) 154 155 done, _ = wait(format_futures, return_when=FIRST_EXCEPTION) 156 for future in done: 157 if exception := future.exception(): 158 executor.shutdown(wait=True, cancel_futures=True) 159 sys.exit(exception.args[0]) 160 161if __name__ == '__main__': 162 main() 163