Skip to content

Commit 94984da

Browse files
committed
[Workflow] Add new code format helper.
This helper will format python files with black/darker and C/C++ files with clang-format. The format helper is written so that we can expand it with new formatters in the future like clang-tidy. Demo of the output is here: tru#8
1 parent 917392a commit 94984da

File tree

5 files changed

+345
-39
lines changed

5 files changed

+345
-39
lines changed

.github/workflows/pr-code-format.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: "Check code formatting"
2+
on: pull_request
3+
permissions:
4+
pull-requests: write
5+
6+
jobs:
7+
code_formatter:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Fetch LLVM sources
11+
uses: actions/checkout@v4
12+
with:
13+
persist-credentials: false
14+
fetch-depth: 2
15+
16+
- name: Get changed files
17+
id: changed-files
18+
uses: tj-actions/changed-files@v39
19+
with:
20+
separator: ","
21+
22+
- name: "Listed files"
23+
run: |
24+
echo "Formatting files:"
25+
echo "${{ steps.changed-files.outputs.all_changed_files }}"
26+
27+
- name: Install clang-format
28+
uses: aminya/setup-cpp@v1
29+
with:
30+
clangformat: 16.0.6
31+
32+
- name: Setup Python env
33+
uses: actions/setup-python@v4
34+
with:
35+
python-version: '3.11'
36+
cache: 'pip'
37+
cache-dependency-path: 'llvm/utils/git/requirements_formatting.txt'
38+
39+
- name: Install python dependencies
40+
run: pip install -r llvm/utils/git/requirements_formatting.txt
41+
42+
- name: Run code formatter
43+
env:
44+
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
45+
START_REV: ${{ github.event.pull_request.base.sha }}
46+
END_REV: ${{ github.event.pull_request.head.sha }}
47+
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
48+
run: |
49+
python llvm/utils/git/code-format-helper.py \
50+
--token ${{ secrets.GITHUB_TOKEN }} \
51+
--issue-number $GITHUB_PR_NUMBER \
52+
--start-rev $START_REV \
53+
--end-rev $END_REV \
54+
--changed-files "$CHANGED_FILES"

.github/workflows/pr-python-format.yml

Lines changed: 0 additions & 39 deletions
This file was deleted.

llvm/utils/git/code-format-helper.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env python3
2+
#
3+
# ====- code-format-helper, runs code formatters from the ci --*- 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+
import argparse
12+
import os
13+
import subprocess
14+
import sys
15+
from functools import cached_property
16+
17+
import github
18+
from github import IssueComment, PullRequest
19+
20+
21+
class FormatHelper:
22+
COMMENT_TAG = "<!--LLVM CODE FORMAT COMMENT: {fmt}-->"
23+
name = "unknown"
24+
25+
@property
26+
def comment_tag(self) -> str:
27+
return self.COMMENT_TAG.replace("fmt", self.name)
28+
29+
def format_run(self, changed_files: [str], args: argparse.Namespace) -> str | None:
30+
pass
31+
32+
def pr_comment_text(self, diff: str) -> str:
33+
return f"""
34+
{self.comment_tag}
35+
36+
:warning: {self.friendly_name}, {self.name} found issues in your code. :warning:
37+
38+
<details>
39+
<summary>
40+
You can test this locally with the following command:
41+
</summary>
42+
43+
``````````bash
44+
{self.instructions}
45+
``````````
46+
47+
</details>
48+
49+
<details>
50+
<summary>
51+
View the diff from {self.name} here.
52+
</summary>
53+
54+
``````````diff
55+
{diff}
56+
``````````
57+
58+
</details>
59+
"""
60+
61+
def find_comment(
62+
self, pr: PullRequest.PullRequest
63+
) -> IssueComment.IssueComment | None:
64+
for comment in pr.as_issue().get_comments():
65+
if self.comment_tag in comment.body:
66+
return comment
67+
return None
68+
69+
def update_pr(self, diff: str, args: argparse.Namespace):
70+
repo = github.Github(args.token).get_repo(args.repo)
71+
pr = repo.get_issue(args.issue_number).as_pull_request()
72+
73+
existing_comment = self.find_comment(pr)
74+
pr_text = self.pr_comment_text(diff)
75+
76+
if existing_comment:
77+
existing_comment.edit(pr_text)
78+
else:
79+
pr.as_issue().create_comment(pr_text)
80+
81+
def update_pr_success(self, args: argparse.Namespace):
82+
repo = github.Github(args.token).get_repo(args.repo)
83+
pr = repo.get_issue(args.issue_number).as_pull_request()
84+
85+
existing_comment = self.find_comment(pr)
86+
if existing_comment:
87+
existing_comment.edit(
88+
f"""
89+
{self.comment_tag}
90+
:white_check_mark: With the latest revision this PR passed the {self.friendly_name}.
91+
"""
92+
)
93+
94+
def run(self, changed_files: [str], args: argparse.Namespace):
95+
diff = self.format_run(changed_files, args)
96+
if diff:
97+
self.update_pr(diff, args)
98+
return False
99+
else:
100+
self.update_pr_success(args)
101+
return True
102+
103+
104+
class ClangFormatHelper(FormatHelper):
105+
name = "clang-format"
106+
friendly_name = "C/C++ code formatter"
107+
108+
@property
109+
def instructions(self):
110+
return " ".join(self.cf_cmd)
111+
112+
@cached_property
113+
def libcxx_excluded_files(self):
114+
with open("libcxx/utils/data/ignore_format.txt", "r") as ifd:
115+
return [excl.strip() for excl in ifd.readlines()]
116+
117+
def should_be_excluded(self, path: str) -> bool:
118+
for excl in self.libcxx_excluded_files:
119+
if path == excl:
120+
print(f"Excluding file {path}")
121+
return True
122+
return False
123+
124+
def filter_changed_files(self, changed_files: [str]) -> [str]:
125+
filtered_files = []
126+
for path in changed_files:
127+
_, ext = os.path.splitext(path)
128+
if ext in (".cpp", ".c", ".h", ".hpp", ".hxx", ".cxx"):
129+
if not self.should_be_excluded(path):
130+
filtered_files.append(path)
131+
return filtered_files
132+
133+
def format_run(self, changed_files: [str], args: argparse.Namespace) -> str | None:
134+
self.args = args
135+
cpp_files = self.filter_changed_files(changed_files)
136+
if not cpp_files:
137+
return
138+
cf_cmd = [
139+
"git-clang-format",
140+
"--diff",
141+
args.start_rev,
142+
args.end_rev,
143+
"--",
144+
] + cpp_files
145+
print(f"Running: {' '.join(cf_cmd)}")
146+
self.cf_cmd = cf_cmd
147+
proc = subprocess.run(cf_cmd, capture_output=True)
148+
149+
# formatting needed
150+
if proc.returncode == 1:
151+
return proc.stdout.decode("utf-8")
152+
153+
return None
154+
155+
156+
class DarkerFormatHelper(FormatHelper):
157+
name = "darker"
158+
friendly_name = "Python code formatter"
159+
160+
@property
161+
def instructions(self):
162+
return " ".join(self.darker_cmd)
163+
164+
def filter_changed_files(self, changed_files: [str]) -> [str]:
165+
filtered_files = []
166+
for path in changed_files:
167+
name, ext = os.path.splitext(path)
168+
if ext == ".py":
169+
filtered_files.append(path)
170+
171+
return filtered_files
172+
173+
def format_run(self, changed_files: [str], args: argparse.Namespace) -> str | None:
174+
self.args = args
175+
py_files = self.filter_changed_files(changed_files)
176+
if not py_files:
177+
return
178+
darker_cmd = [
179+
"darker",
180+
"--check",
181+
"--diff",
182+
"-r",
183+
f"{args.start_rev}..{args.end_rev}",
184+
] + py_files
185+
print(f"Running: {' '.join(darker_cmd)}")
186+
self.darker_cmd = darker_cmd
187+
proc = subprocess.run(darker_cmd, capture_output=True)
188+
189+
# formatting needed
190+
if proc.returncode == 1:
191+
return proc.stdout.decode("utf-8")
192+
193+
return None
194+
195+
196+
ALL_FORMATTERS = (DarkerFormatHelper(), ClangFormatHelper())
197+
198+
if __name__ == "__main__":
199+
parser = argparse.ArgumentParser()
200+
parser.add_argument(
201+
"--token", type=str, required=True, help="GitHub authentiation token"
202+
)
203+
parser.add_argument(
204+
"--repo",
205+
type=str,
206+
default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"),
207+
help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)",
208+
)
209+
parser.add_argument("--issue-number", type=int, required=True)
210+
parser.add_argument(
211+
"--start-rev",
212+
type=str,
213+
required=True,
214+
help="Compute changes from this revision.",
215+
)
216+
parser.add_argument(
217+
"--end-rev", type=str, required=True, help="Compute changes to this revision"
218+
)
219+
parser.add_argument(
220+
"--changed-files",
221+
type=str,
222+
help="Comma separated list of files that has been changed",
223+
)
224+
225+
args = parser.parse_args()
226+
227+
changed_files = []
228+
if args.changed_files:
229+
changed_files = args.changed_files.split(",")
230+
231+
exit_code = 0
232+
for fmt in ALL_FORMATTERS:
233+
if not fmt.run(changed_files, args):
234+
exit_code = 1
235+
236+
sys.exit(exit_code)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.11
3+
# by the following command:
4+
#
5+
# pip-compile --output-file=llvm/utils/git/requirements_formatting.txt llvm/utils/git/requirements_formatting.txt.in
6+
#
7+
black==23.9.1
8+
# via
9+
# -r llvm/utils/git/requirements_formatting.txt.in
10+
# darker
11+
certifi==2023.7.22
12+
# via requests
13+
cffi==1.15.1
14+
# via
15+
# cryptography
16+
# pynacl
17+
charset-normalizer==3.2.0
18+
# via requests
19+
click==8.1.7
20+
# via black
21+
cryptography==41.0.3
22+
# via pyjwt
23+
darker==1.7.2
24+
# via -r llvm/utils/git/requirements_formatting.txt.in
25+
deprecated==1.2.14
26+
# via pygithub
27+
idna==3.4
28+
# via requests
29+
mypy-extensions==1.0.0
30+
# via black
31+
packaging==23.1
32+
# via black
33+
pathspec==0.11.2
34+
# via black
35+
platformdirs==3.10.0
36+
# via black
37+
pycparser==2.21
38+
# via cffi
39+
pygithub==1.59.1
40+
# via -r llvm/utils/git/requirements_formatting.txt.in
41+
pyjwt[crypto]==2.8.0
42+
# via pygithub
43+
pynacl==1.5.0
44+
# via pygithub
45+
requests==2.31.0
46+
# via pygithub
47+
toml==0.10.2
48+
# via darker
49+
urllib3==2.0.4
50+
# via requests
51+
wrapt==1.15.0
52+
# via deprecated
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
black~=23.0
2+
darker==1.7.2
3+
PyGithub==1.59.1

0 commit comments

Comments
 (0)