• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""
12This script reads input from a unified diff and reformats all the changed
13lines. This is useful to reformat all the lines touched by a specific patch.
14Example 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"""
20from __future__ import absolute_import, division, print_function
21
22import argparse
23import difflib
24import re
25import subprocess
26import sys
27
28if sys.version_info.major >= 3:
29    from io import StringIO
30else:
31    from io import BytesIO as StringIO
32
33
34def 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
165if __name__ == "__main__":
166    main()
167