|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | # |
3 | | -#===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===# |
| 3 | +# ===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===# |
4 | 4 | # |
5 | | -# The LLVM Compiler Infrastructure |
| 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 |
6 | 8 | # |
7 | | -# This file is distributed under the University of Illinois Open Source |
8 | | -# License. |
9 | | -# |
10 | | -# ============================================================ |
11 | | -# |
12 | | -# University of Illinois/NCSA |
13 | | -# Open Source License |
14 | | -# |
15 | | -# Copyright (c) 2007-2015 University of Illinois at Urbana-Champaign. |
16 | | -# All rights reserved. |
17 | | -# |
18 | | -# Developed by: |
19 | | -# |
20 | | -# LLVM Team |
21 | | -# |
22 | | -# University of Illinois at Urbana-Champaign |
23 | | -# |
24 | | -# http://llvm.org |
25 | | -# |
26 | | -# Permission is hereby granted, free of charge, to any person obtaining a copy of |
27 | | -# this software and associated documentation files (the "Software"), to deal with |
28 | | -# the Software without restriction, including without limitation the rights to |
29 | | -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
30 | | -# of the Software, and to permit persons to whom the Software is furnished to do |
31 | | -# so, subject to the following conditions: |
32 | | -# |
33 | | -# * Redistributions of source code must retain the above copyright notice, |
34 | | -# this list of conditions and the following disclaimers. |
35 | | -# |
36 | | -# * Redistributions in binary form must reproduce the above copyright notice, |
37 | | -# this list of conditions and the following disclaimers in the |
38 | | -# documentation and/or other materials provided with the distribution. |
39 | | -# |
40 | | -# * Neither the names of the LLVM Team, University of Illinois at |
41 | | -# Urbana-Champaign, nor the names of its contributors may be used to |
42 | | -# endorse or promote products derived from this Software without specific |
43 | | -# prior written permission. |
44 | | -# |
45 | | -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
46 | | -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
47 | | -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
48 | | -# CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
49 | | -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
50 | | -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE |
51 | | -# SOFTWARE. |
52 | | -# |
53 | | -# ============================================================ |
54 | | -# |
55 | | -#===------------------------------------------------------------------------===# |
56 | | - |
57 | | -r""" |
58 | | -ClangFormat Diff Reformatter |
59 | | -============================ |
| 9 | +# ===------------------------------------------------------------------------===# |
60 | 10 |
|
| 11 | +""" |
61 | 12 | This script reads input from a unified diff and reformats all the changed |
62 | 13 | lines. This is useful to reformat all the lines touched by a specific patch. |
63 | 14 | Example usage for git/svn users: |
64 | 15 |
|
65 | | - git diff -U0 HEAD^ | clang-format-diff.py -p1 -i |
66 | | - svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i |
| 16 | + git diff -U0 --no-color --relative HEAD^ | {clang_format_diff} -p1 -i |
| 17 | + svn diff --diff-cmd=diff -x-U0 | {clang_format_diff} -i |
67 | 18 |
|
| 19 | +It should be noted that the filename contained in the diff is used unmodified |
| 20 | +to determine the source file to update. Users calling this script directly |
| 21 | +should be careful to ensure that the path in the diff is correct relative to the |
| 22 | +current working directory. |
68 | 23 | """ |
| 24 | +from __future__ import absolute_import, division, print_function |
69 | 25 |
|
70 | 26 | import argparse |
71 | 27 | import difflib |
72 | | -import io |
73 | 28 | import re |
74 | 29 | import subprocess |
75 | 30 | import sys |
76 | 31 |
|
77 | | - |
78 | | -# Change this to the full path if clang-format is not on the path. |
79 | | -binary = 'clang-format' |
| 32 | +if sys.version_info.major >= 3: |
| 33 | + from io import StringIO |
| 34 | +else: |
| 35 | + from io import BytesIO as StringIO |
80 | 36 |
|
81 | 37 |
|
82 | 38 | def main(): |
83 | | - parser = argparse.ArgumentParser(description= |
84 | | - 'Reformat changed lines in diff. Without -i ' |
85 | | - 'option just output the diff that would be ' |
86 | | - 'introduced.') |
87 | | - parser.add_argument('-i', action='store_true', default=False, |
88 | | - help='apply edits to files instead of displaying a diff') |
89 | | - parser.add_argument('-p', metavar='NUM', default=0, |
90 | | - help='strip the smallest prefix containing P slashes') |
91 | | - parser.add_argument('-regex', metavar='PATTERN', default=None, |
92 | | - help='custom pattern selecting file paths to reformat ' |
93 | | - '(case sensitive, overrides -iregex)') |
94 | | - parser.add_argument('-iregex', metavar='PATTERN', default= |
95 | | - r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc|js|ts|proto' |
96 | | - r'|protodevel|java)', |
97 | | - help='custom pattern selecting file paths to reformat ' |
98 | | - '(case insensitive, overridden by -regex)') |
99 | | - parser.add_argument('-sort-includes', action='store_true', default=False, |
100 | | - help='let clang-format sort include blocks') |
101 | | - parser.add_argument('-v', '--verbose', action='store_true', |
102 | | - help='be more verbose, ineffective without -i') |
103 | | - args = parser.parse_args() |
104 | | - |
105 | | - # Extract changed lines for each file. |
106 | | - filename = None |
107 | | - lines_by_file = {} |
108 | | - for line in sys.stdin: |
109 | | - match = re.search(r'^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line) |
110 | | - if match: |
111 | | - filename = match.group(2) |
112 | | - if filename is None: |
113 | | - continue |
114 | | - |
115 | | - if args.regex is not None: |
116 | | - if not re.match('^%s$' % args.regex, filename): |
117 | | - continue |
118 | | - else: |
119 | | - if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): |
120 | | - continue |
121 | | - |
122 | | - match = re.search(r'^@@.*\+(\d+)(,(\d+))?', line) |
123 | | - if match: |
124 | | - start_line = int(match.group(1)) |
125 | | - line_count = 1 |
126 | | - if match.group(3): |
127 | | - line_count = int(match.group(3)) |
128 | | - if line_count == 0: |
129 | | - continue |
130 | | - end_line = start_line + line_count - 1 |
131 | | - lines_by_file.setdefault(filename, []).extend( |
132 | | - ['-lines', str(start_line) + ':' + str(end_line)]) |
133 | | - |
134 | | - # Reformat files containing changes in place. |
135 | | - for filename, lines in lines_by_file.items(): |
136 | | - if args.i and args.verbose: |
137 | | - print('Formatting {}'.format(filename)) |
138 | | - command = [binary, filename] |
139 | | - if args.i: |
140 | | - command.append('-i') |
141 | | - if args.sort_includes: |
142 | | - command.append('-sort-includes') |
143 | | - command.extend(lines) |
144 | | - command.extend(['-style=file', '-fallback-style=none']) |
145 | | - p = subprocess.Popen(command, |
146 | | - stdout=subprocess.PIPE, |
147 | | - stderr=None, |
148 | | - stdin=subprocess.PIPE, |
149 | | - text=True) |
150 | | - stdout, stderr = p.communicate() |
151 | | - if p.returncode != 0: |
152 | | - sys.exit(p.returncode) |
153 | | - |
154 | | - if not args.i: |
155 | | - with open(filename, encoding="utf8") as f: |
156 | | - code = f.readlines() |
157 | | - formatted_code = io.StringIO(stdout).readlines() |
158 | | - diff = difflib.unified_diff(code, formatted_code, |
159 | | - filename, filename, |
160 | | - '(before formatting)', '(after formatting)') |
161 | | - diff_string = ''.join(diff) |
162 | | - if len(diff_string) > 0: |
163 | | - sys.stdout.write(diff_string) |
164 | | - |
165 | | -if __name__ == '__main__': |
166 | | - main() |
| 39 | + parser = argparse.ArgumentParser( |
| 40 | + description=__doc__.format(clang_format_diff="%(prog)s"), |
| 41 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 42 | + ) |
| 43 | + parser.add_argument( |
| 44 | + "-i", |
| 45 | + action="store_true", |
| 46 | + default=False, |
| 47 | + help="apply edits to files instead of displaying a diff", |
| 48 | + ) |
| 49 | + parser.add_argument( |
| 50 | + "-p", |
| 51 | + metavar="NUM", |
| 52 | + default=0, |
| 53 | + help="strip the smallest prefix containing P slashes", |
| 54 | + ) |
| 55 | + parser.add_argument( |
| 56 | + "-regex", |
| 57 | + metavar="PATTERN", |
| 58 | + default=None, |
| 59 | + help="custom pattern selecting file paths to reformat " |
| 60 | + "(case sensitive, overrides -iregex)", |
| 61 | + ) |
| 62 | + parser.add_argument( |
| 63 | + "-iregex", |
| 64 | + metavar="PATTERN", |
| 65 | + default=r".*\.(?:cpp|cc|c\+\+|cxx|cppm|ccm|cxxm|c\+\+m|c|cl|h|hh|hpp" |
| 66 | + r"|hxx|m|mm|inc|js|ts|proto|protodevel|java|cs|json|s?vh?)", |
| 67 | + help="custom pattern selecting file paths to reformat " |
| 68 | + "(case insensitive, overridden by -regex)", |
| 69 | + ) |
| 70 | + parser.add_argument( |
| 71 | + "-sort-includes", |
| 72 | + action="store_true", |
| 73 | + default=False, |
| 74 | + help="let clang-format sort include blocks", |
| 75 | + ) |
| 76 | + parser.add_argument( |
| 77 | + "-v", |
| 78 | + "--verbose", |
| 79 | + action="store_true", |
| 80 | + help="be more verbose, ineffective without -i", |
| 81 | + ) |
| 82 | + parser.add_argument( |
| 83 | + "-style", |
| 84 | + help="formatting style to apply (LLVM, GNU, Google, Chromium, " |
| 85 | + "Microsoft, Mozilla, WebKit)", |
| 86 | + ) |
| 87 | + parser.add_argument( |
| 88 | + "-fallback-style", |
| 89 | + help="The name of the predefined style used as a" |
| 90 | + "fallback in case clang-format is invoked with" |
| 91 | + "-style=file, but can not find the .clang-format" |
| 92 | + "file to use.", |
| 93 | + ) |
| 94 | + parser.add_argument( |
| 95 | + "-binary", |
| 96 | + default="clang-format", |
| 97 | + help="location of binary to use for clang-format", |
| 98 | + ) |
| 99 | + args = parser.parse_args() |
| 100 | + |
| 101 | + # Extract changed lines for each file. |
| 102 | + filename = None |
| 103 | + lines_by_file = {} |
| 104 | + for line in sys.stdin: |
| 105 | + match = re.search(r"^\+\+\+\ (.*?/){%s}(\S*)" % args.p, line) |
| 106 | + if match: |
| 107 | + filename = match.group(2) |
| 108 | + if filename is None: |
| 109 | + continue |
| 110 | + |
| 111 | + if args.regex is not None: |
| 112 | + if not re.match("^%s$" % args.regex, filename): |
| 113 | + continue |
| 114 | + else: |
| 115 | + if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE): |
| 116 | + continue |
| 117 | + |
| 118 | + match = re.search(r"^@@.*\+(\d+)(?:,(\d+))?", line) |
| 119 | + if match: |
| 120 | + start_line = int(match.group(1)) |
| 121 | + line_count = 1 |
| 122 | + if match.group(2): |
| 123 | + line_count = int(match.group(2)) |
| 124 | + # The input is something like |
| 125 | + # |
| 126 | + # @@ -1, +0,0 @@ |
| 127 | + # |
| 128 | + # which means no lines were added. |
| 129 | + if line_count == 0: |
| 130 | + continue |
| 131 | + # Also format lines range if line_count is 0 in case of deleting |
| 132 | + # surrounding statements. |
| 133 | + end_line = start_line |
| 134 | + if line_count != 0: |
| 135 | + end_line += line_count - 1 |
| 136 | + lines_by_file.setdefault(filename, []).extend( |
| 137 | + ["-lines", str(start_line) + ":" + str(end_line)] |
| 138 | + ) |
| 139 | + |
| 140 | + # Reformat files containing changes in place. |
| 141 | + for filename, lines in lines_by_file.items(): |
| 142 | + if args.i and args.verbose: |
| 143 | + print("Formatting {}".format(filename)) |
| 144 | + command = [args.binary, filename] |
| 145 | + if args.i: |
| 146 | + command.append("-i") |
| 147 | + if args.sort_includes: |
| 148 | + command.append("-sort-includes") |
| 149 | + command.extend(lines) |
| 150 | + if args.style: |
| 151 | + command.extend(["-style", args.style]) |
| 152 | + if args.fallback_style: |
| 153 | + command.extend(["-fallback-style", args.fallback_style]) |
| 154 | + |
| 155 | + try: |
| 156 | + p = subprocess.Popen( |
| 157 | + command, |
| 158 | + stdout=subprocess.PIPE, |
| 159 | + stderr=None, |
| 160 | + stdin=subprocess.PIPE, |
| 161 | + universal_newlines=True, |
| 162 | + ) |
| 163 | + except OSError as e: |
| 164 | + # Give the user more context when clang-format isn't |
| 165 | + # found/isn't executable, etc. |
| 166 | + raise RuntimeError( |
| 167 | + 'Failed to run "%s" - %s"' % (" ".join(command), e.strerror) |
| 168 | + ) |
| 169 | + |
| 170 | + stdout, stderr = p.communicate() |
| 171 | + if p.returncode != 0: |
| 172 | + sys.exit(p.returncode) |
| 173 | + |
| 174 | + if not args.i: |
| 175 | + with open(filename) as f: |
| 176 | + code = f.readlines() |
| 177 | + formatted_code = StringIO(stdout).readlines() |
| 178 | + diff = difflib.unified_diff( |
| 179 | + code, |
| 180 | + formatted_code, |
| 181 | + filename, |
| 182 | + filename, |
| 183 | + "(before formatting)", |
| 184 | + "(after formatting)", |
| 185 | + ) |
| 186 | + diff_string = "".join(diff) |
| 187 | + if len(diff_string) > 0: |
| 188 | + sys.stdout.write(diff_string) |
| 189 | + sys.exit(1) |
| 190 | + |
| 191 | + |
| 192 | +if __name__ == "__main__": |
| 193 | + main() |
0 commit comments