|
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 | +from io import StringIO |
80 | 33 |
|
81 | 34 |
|
82 | 35 | 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() |
| 36 | + parser = argparse.ArgumentParser( |
| 37 | + description=__doc__.format(clang_format_diff="%(prog)s"), |
| 38 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 39 | + ) |
| 40 | + parser.add_argument( |
| 41 | + "-i", |
| 42 | + action="store_true", |
| 43 | + default=False, |
| 44 | + help="apply edits to files instead of displaying a diff", |
| 45 | + ) |
| 46 | + parser.add_argument( |
| 47 | + "-p", |
| 48 | + metavar="NUM", |
| 49 | + default=0, |
| 50 | + help="strip the smallest prefix containing P slashes", |
| 51 | + ) |
| 52 | + parser.add_argument( |
| 53 | + "-regex", |
| 54 | + metavar="PATTERN", |
| 55 | + default=None, |
| 56 | + help="custom pattern selecting file paths to reformat " |
| 57 | + "(case sensitive, overrides -iregex)", |
| 58 | + ) |
| 59 | + parser.add_argument( |
| 60 | + "-iregex", |
| 61 | + metavar="PATTERN", |
| 62 | + default=r".*\.(?:cpp|cc|c\+\+|cxx|cppm|ccm|cxxm|c\+\+m|c|cl|h|hh|hpp" |
| 63 | + r"|hxx|m|mm|inc|js|ts|proto|protodevel|java|cs|json|s?vh?)", |
| 64 | + help="custom pattern selecting file paths to reformat " |
| 65 | + "(case insensitive, overridden by -regex)", |
| 66 | + ) |
| 67 | + parser.add_argument( |
| 68 | + "-sort-includes", |
| 69 | + action="store_true", |
| 70 | + default=False, |
| 71 | + help="let clang-format sort include blocks", |
| 72 | + ) |
| 73 | + parser.add_argument( |
| 74 | + "-v", |
| 75 | + "--verbose", |
| 76 | + action="store_true", |
| 77 | + help="be more verbose, ineffective without -i", |
| 78 | + ) |
| 79 | + parser.add_argument( |
| 80 | + "-style", |
| 81 | + help="formatting style to apply (LLVM, GNU, Google, Chromium, " |
| 82 | + "Microsoft, Mozilla, WebKit)", |
| 83 | + ) |
| 84 | + parser.add_argument( |
| 85 | + "-fallback-style", |
| 86 | + help="The name of the predefined style used as a" |
| 87 | + "fallback in case clang-format is invoked with" |
| 88 | + "-style=file, but can not find the .clang-format" |
| 89 | + "file to use.", |
| 90 | + ) |
| 91 | + parser.add_argument( |
| 92 | + "-binary", |
| 93 | + default="clang-format", |
| 94 | + help="location of binary to use for clang-format", |
| 95 | + ) |
| 96 | + args = parser.parse_args() |
| 97 | + |
| 98 | + # Extract changed lines for each file. |
| 99 | + filename = None |
| 100 | + lines_by_file = {} |
| 101 | + for line in sys.stdin: |
| 102 | + match = re.search(r"^\+\+\+\ (.*?/){%s}(\S*)" % args.p, line) |
| 103 | + if match: |
| 104 | + filename = match.group(2) |
| 105 | + if filename is None: |
| 106 | + continue |
| 107 | + |
| 108 | + if args.regex is not None: |
| 109 | + if not re.match("^%s$" % args.regex, filename): |
| 110 | + continue |
| 111 | + else: |
| 112 | + if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE): |
| 113 | + continue |
| 114 | + |
| 115 | + match = re.search(r"^@@.*\+(\d+)(?:,(\d+))?", line) |
| 116 | + if match: |
| 117 | + start_line = int(match.group(1)) |
| 118 | + line_count = 1 |
| 119 | + if match.group(2): |
| 120 | + line_count = int(match.group(2)) |
| 121 | + # The input is something like |
| 122 | + # |
| 123 | + # @@ -1, +0,0 @@ |
| 124 | + # |
| 125 | + # which means no lines were added. |
| 126 | + if line_count == 0: |
| 127 | + continue |
| 128 | + # Also format lines range if line_count is 0 in case of deleting |
| 129 | + # surrounding statements. |
| 130 | + end_line = start_line |
| 131 | + if line_count != 0: |
| 132 | + end_line += line_count - 1 |
| 133 | + lines_by_file.setdefault(filename, []).extend( |
| 134 | + ["-lines", str(start_line) + ":" + str(end_line)] |
| 135 | + ) |
| 136 | + |
| 137 | + # Reformat files containing changes in place. |
| 138 | + for filename, lines in lines_by_file.items(): |
| 139 | + if args.i and args.verbose: |
| 140 | + print("Formatting {}".format(filename)) |
| 141 | + command = [args.binary, filename] |
| 142 | + if args.i: |
| 143 | + command.append("-i") |
| 144 | + if args.sort_includes: |
| 145 | + command.append("-sort-includes") |
| 146 | + command.extend(lines) |
| 147 | + if args.style: |
| 148 | + command.extend(["-style", args.style]) |
| 149 | + if args.fallback_style: |
| 150 | + command.extend(["-fallback-style", args.fallback_style]) |
| 151 | + |
| 152 | + try: |
| 153 | + p = subprocess.Popen( |
| 154 | + command, |
| 155 | + stdout=subprocess.PIPE, |
| 156 | + stderr=None, |
| 157 | + stdin=subprocess.PIPE, |
| 158 | + universal_newlines=True, |
| 159 | + ) |
| 160 | + except OSError as e: |
| 161 | + # Give the user more context when clang-format isn't |
| 162 | + # found/isn't executable, etc. |
| 163 | + raise RuntimeError( |
| 164 | + 'Failed to run "%s" - %s"' % (" ".join(command), e.strerror) |
| 165 | + ) |
| 166 | + |
| 167 | + stdout, stderr = p.communicate() |
| 168 | + if p.returncode != 0: |
| 169 | + sys.exit(p.returncode) |
| 170 | + |
| 171 | + if not args.i: |
| 172 | + with open(filename, encoding="utf8") as f: |
| 173 | + code = f.readlines() |
| 174 | + formatted_code = StringIO(stdout).readlines() |
| 175 | + diff = difflib.unified_diff( |
| 176 | + code, |
| 177 | + formatted_code, |
| 178 | + filename, |
| 179 | + filename, |
| 180 | + "(before formatting)", |
| 181 | + "(after formatting)", |
| 182 | + ) |
| 183 | + diff_string = "".join(diff) |
| 184 | + if len(diff_string) > 0: |
| 185 | + sys.stdout.write(diff_string) |
| 186 | + sys.exit(1) |
| 187 | + |
| 188 | + |
| 189 | +if __name__ == "__main__": |
| 190 | + main() |
0 commit comments