Skip to content

Commit 4f18f80

Browse files
GaryOderNichtsWinterMute
authored andcommitted
Add formatting check workflow
Bundle clang-format-diff with workflow
1 parent 2b66122 commit 4f18f80

2 files changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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 --relative HEAD^ | {clang_format_diff} -p1 -i
17+
svn diff --diff-cmd=diff -x-U0 | {clang_format_diff} -i
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.
23+
"""
24+
from __future__ import absolute_import, division, print_function
25+
26+
import argparse
27+
import difflib
28+
import re
29+
import subprocess
30+
import sys
31+
32+
if sys.version_info.major >= 3:
33+
from io import StringIO
34+
else:
35+
from io import BytesIO as StringIO
36+
37+
38+
def 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+
has_diff = False
142+
for filename, lines in lines_by_file.items():
143+
if args.i and args.verbose:
144+
print("Formatting {}".format(filename))
145+
command = [args.binary, filename]
146+
if args.i:
147+
command.append("-i")
148+
if args.sort_includes:
149+
command.append("--sort-includes")
150+
command.extend(lines)
151+
if args.style:
152+
command.extend(["--style", args.style])
153+
if args.fallback_style:
154+
command.extend(["--fallback-style", args.fallback_style])
155+
156+
try:
157+
p = subprocess.Popen(
158+
command,
159+
stdout=subprocess.PIPE,
160+
stderr=None,
161+
stdin=subprocess.PIPE,
162+
universal_newlines=True,
163+
)
164+
except OSError as e:
165+
# Give the user more context when clang-format isn't
166+
# found/isn't executable, etc.
167+
raise RuntimeError(
168+
'Failed to run "%s" - %s"' % (" ".join(command), e.strerror)
169+
)
170+
171+
stdout, _stderr = p.communicate()
172+
if p.returncode != 0:
173+
return p.returncode
174+
175+
if not args.i:
176+
with open(filename) as f:
177+
code = f.readlines()
178+
formatted_code = StringIO(stdout).readlines()
179+
diff = difflib.unified_diff(
180+
code,
181+
formatted_code,
182+
filename,
183+
filename,
184+
"(before formatting)",
185+
"(after formatting)",
186+
)
187+
diff_string = "".join(diff)
188+
if len(diff_string) > 0:
189+
has_diff = True
190+
sys.stdout.write(diff_string)
191+
192+
if has_diff:
193+
return 1
194+
195+
196+
if __name__ == "__main__":
197+
sys.exit(main())

.github/workflows/format-check.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Formatting check
2+
3+
on: [pull_request]
4+
5+
jobs:
6+
formatting-check:
7+
name: Formatting check
8+
runs-on: ubuntu-24.04
9+
10+
steps:
11+
- uses: actions/checkout@v4
12+
with:
13+
# To get a diff from the PR we need to fetch 2 commits.
14+
# The checkout action will create a merge commit as {{ github.sha }}.
15+
fetch-depth: 2
16+
17+
- name: Install dependencies
18+
run: |
19+
sudo apt-get update
20+
sudo apt-get install -yqq clang-format-18
21+
22+
- name: Format check
23+
id: format-check
24+
run: |
25+
git diff -U0 --no-color ${{ github.sha }}^ -- '*.cpp' '*.c' '*.h' '*.hpp' |
26+
./.github/workflows/clang-format-diff.py -p1 -binary clang-format-18 > clang-format.patch || true
27+
28+
# Check if patch is not empty
29+
if [ -s clang-format.patch ]; then
30+
echo "###############################################################"
31+
echo "# Format checks failed!"
32+
echo "# A patch has been uploaded as an artifact and is shown below."
33+
echo "###############################################################"
34+
35+
# Show patch
36+
cat clang-format.patch
37+
38+
exit 1
39+
fi
40+
41+
- name: Upload format fixes patch
42+
uses: actions/upload-artifact@v4
43+
if: ${{ failure() && steps.format-check.outcome == 'failure' }}
44+
with:
45+
name: clang-format.patch
46+
path: clang-format.patch
47+
if-no-files-found: ignore

0 commit comments

Comments
 (0)