1 #!/usr/bin/env python3
2 #
3 # === clang-format-diff.py - ClangFormat Diff Reformatter ---*- python -*-=== #
4 #
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
8 #
9 # ===---------------------------------------------------------------------=== #
10
11 """
12 This script reads input from a unified diff and reformats all the changed
13 lines. This is useful to reformat all the lines touched by a specific patch.
14 Example usage for git/svn users:
15
16 git diff -U0 --no-color HEAD^ | clang-format-diff.py -p1 -i
17 svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i
18
19 """
20 from __future__ import absolute_import, division, print_function
21
22 import argparse
23 import difflib
24 import re
25 import subprocess
26 import sys
27
28 if sys.version_info.major >= 3:
29 from io import StringIO
30 else:
31 from io import BytesIO as StringIO
32
33
34 def main():
35 parser = argparse.ArgumentParser(
36 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
37 )
38 parser.add_argument(
39 "-i",
40 action="store_true",
41 default=False,
42 help="apply edits to files instead of displaying a " "diff",
43 )
44 parser.add_argument(
45 "-p",
46 metavar="NUM",
47 default=0,
48 help="strip the smallest prefix containing P slashes",
49 )
50 parser.add_argument(
51 "-regex",
52 metavar="PATTERN",
53 default=None,
54 help="custom pattern selecting file paths to reformat "
55 "(case sensitive, overrides -iregex)",
56 )
57 parser.add_argument(
58 "-iregex",
59 metavar="PATTERN",
60 default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hh|hpp|m|mm|inc"
61 r"|js|ts|proto|protodevel|java|cs)",
62 help="custom pattern selecting file paths to reformat "
63 "(case insensitive, overridden by -regex)",
64 )
65 parser.add_argument(
66 "-sort-includes",
67 action="store_true",
68 default=False,
69 help="let clang-format sort include blocks",
70 )
71 parser.add_argument(
72 "-v",
73 "--verbose",
74 action="store_true",
75 help="be more verbose, ineffective without -i",
76 )
77 parser.add_argument(
78 "-style",
79 help="formatting style to apply (LLVM, Google, " "Chromium, Mozilla, WebKit)",
80 )
81 parser.add_argument(
82 "-binary",
83 default="clang-format",
84 help="location of binary to use for clang-format",
85 )
86 args = parser.parse_args()
87
88 # Extract changed lines for each file.
89 filename = None
90 lines_by_file = {}
91 for line in sys.stdin:
92 match = re.search(r"^\+\+\+\ (.*?/){%s}(\S*)" % args.p, line)
93 if match:
94 filename = match.group(2)
95 if filename is None:
96 continue
97
98 if args.regex is not None:
99 if not re.match("^%s$" % args.regex, filename):
100 continue
101 else:
102 if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE):
103 continue
104
105 match = re.search(r"^@@.*\+(\d+)(,(\d+))?", line)
106 if match:
107 start_line = int(match.group(1))
108 line_count = 1
109 if match.group(3):
110 line_count = int(match.group(3))
111 if line_count == 0:
112 continue
113 end_line = start_line + line_count - 1
114 lines_by_file.setdefault(filename, []).extend(
115 ["-lines", str(start_line) + ":" + str(end_line)]
116 )
117
118 # Reformat files containing changes in place.
119 # We need to count amount of bytes generated in the output of
120 # clang-format-diff. If clang-format-diff doesn't generate any bytes it
121 # means there is nothing to format.
122 format_line_counter = 0
123 for filename, lines in lines_by_file.items():
124 if args.i and args.verbose:
125 print("Formatting {}".format(filename))
126 command = [args.binary, filename]
127 if args.i:
128 command.append("-i")
129 if args.sort_includes:
130 command.append("-sort-includes")
131 command.extend(lines)
132 if args.style:
133 command.extend(["-style", args.style])
134 p = subprocess.Popen(
135 command,
136 stdout=subprocess.PIPE,
137 stderr=None,
138 stdin=subprocess.PIPE,
139 universal_newlines=True,
140 )
141 stdout, _ = p.communicate()
142 if p.returncode != 0:
143 sys.exit(p.returncode)
144
145 if not args.i:
146 with open(filename) as f:
147 code = f.readlines()
148 formatted_code = StringIO(stdout).readlines()
149 diff = difflib.unified_diff(
150 code,
151 formatted_code,
152 filename,
153 filename,
154 "(before formatting)",
155 "(after formatting)",
156 )
157 diff_string = "".join(diff)
158 if diff_string:
159 format_line_counter += sys.stdout.write(diff_string)
160
161 if format_line_counter > 0:
162 sys.exit(1)
163
164
165 if __name__ == "__main__":
166 main()