Skip to content

Commit bd3e8eb

Browse files
authored
code-format: Improve the code-format-helper to be able to run as a git hook (#73957)
As part of #73798 there was some discussion about using the format helper to run from a git-hook. That was not possible for a number of reasons, but with these changes it can now be installed as a hook and then run on the local cache in git instead of a diff between revisions. This also checks for two environment variables DARKER_FORMAT_PATH and CLANG_FORMAT_PATH where you can specify the path to the program you want to use.
1 parent 18959c4 commit bd3e8eb

File tree

1 file changed

+166
-53
lines changed

1 file changed

+166
-53
lines changed

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

100644100755
Lines changed: 166 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,56 @@
11
#!/usr/bin/env python3
22
#
3-
# ====- code-format-helper, runs code formatters from the ci --*- python -*--==#
3+
# ====- code-format-helper, runs code formatters from the ci or in a hook --*- python -*--==#
44
#
55
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
66
# See https://llvm.org/LICENSE.txt for license information.
77
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
88
#
9-
# ==-------------------------------------------------------------------------==#
9+
# ==--------------------------------------------------------------------------------------==#
1010

1111
import argparse
1212
import os
1313
import subprocess
1414
import sys
15-
from functools import cached_property
15+
from typing import List, Optional
1616

17-
import github
18-
from github import IssueComment, PullRequest
17+
"""
18+
This script is run by GitHub actions to ensure that the code in PR's conform to
19+
the coding style of LLVM. It can also be installed as a pre-commit git hook to
20+
check the coding style before submitting it. The canonical source of this script
21+
is in the LLVM source tree under llvm/utils/git.
22+
23+
For C/C++ code it uses clang-format and for Python code it uses darker (which
24+
in turn invokes black).
25+
26+
You can learn more about the LLVM coding style on llvm.org:
27+
https://llvm.org/docs/CodingStandards.html
28+
29+
You can install this script as a git hook by symlinking it to the .git/hooks
30+
directory:
31+
32+
ln -s $(pwd)/llvm/utils/git/code-format-helper.py .git/hooks/pre-commit
33+
34+
You can control the exact path to clang-format or darker with the following
35+
environment variables: $CLANG_FORMAT_PATH and $DARKER_FORMAT_PATH.
36+
"""
37+
38+
39+
class FormatArgs:
40+
start_rev: str = None
41+
end_rev: str = None
42+
repo: str = None
43+
changed_files: List[str] = []
44+
token: str = None
45+
verbose: bool = True
46+
47+
def __init__(self, args: argparse.Namespace = None) -> None:
48+
if not args is None:
49+
self.start_rev = args.start_rev
50+
self.end_rev = args.end_rev
51+
self.repo = args.repo
52+
self.token = args.token
53+
self.changed_files = args.changed_files
1954

2055

2156
class FormatHelper:
@@ -31,9 +66,10 @@ def comment_tag(self) -> str:
3166
def instructions(self) -> str:
3267
raise NotImplementedError()
3368

34-
def format_run(
35-
self, changed_files: list[str], args: argparse.Namespace
36-
) -> str | None:
69+
def has_tool(self) -> bool:
70+
raise NotImplementedError()
71+
72+
def format_run(self, changed_files: List[str], args: FormatArgs) -> Optional[str]:
3773
raise NotImplementedError()
3874

3975
def pr_comment_text_for_diff(self, diff: str) -> str:
@@ -63,17 +99,18 @@ def pr_comment_text_for_diff(self, diff: str) -> str:
6399
</details>
64100
"""
65101

66-
def find_comment(
67-
self, pr: PullRequest.PullRequest
68-
) -> IssueComment.IssueComment | None:
102+
# TODO: any type should be replaced with the correct github type, but it requires refactoring to
103+
# not require the github module to be installed everywhere.
104+
def find_comment(self, pr: any) -> any:
69105
for comment in pr.as_issue().get_comments():
70106
if self.comment_tag in comment.body:
71107
return comment
72108
return None
73109

74-
def update_pr(
75-
self, comment_text: str, args: argparse.Namespace, create_new: bool
76-
) -> None:
110+
def update_pr(self, comment_text: str, args: FormatArgs, create_new: bool) -> None:
111+
import github
112+
from github import IssueComment, PullRequest
113+
77114
repo = github.Github(args.token).get_repo(args.repo)
78115
pr = repo.get_issue(args.issue_number).as_pull_request()
79116

@@ -85,17 +122,25 @@ def update_pr(
85122
elif create_new:
86123
pr.as_issue().create_comment(comment_text)
87124

88-
def run(self, changed_files: list[str], args: argparse.Namespace) -> bool:
125+
def run(self, changed_files: List[str], args: FormatArgs) -> bool:
89126
diff = self.format_run(changed_files, args)
127+
should_update_gh = args.token is not None and args.repo is not None
128+
90129
if diff is None:
91-
comment_text = f"""
92-
:white_check_mark: With the latest revision this PR passed the {self.friendly_name}.
93-
"""
94-
self.update_pr(comment_text, args, create_new=False)
130+
if should_update_gh:
131+
comment_text = f"""
132+
:white_check_mark: With the latest revision this PR passed the {self.friendly_name}.
133+
"""
134+
self.update_pr(comment_text, args, create_new=False)
95135
return True
96136
elif len(diff) > 0:
97-
comment_text = self.pr_comment_text_for_diff(diff)
98-
self.update_pr(comment_text, args, create_new=True)
137+
if should_update_gh:
138+
comment_text = self.pr_comment_text_for_diff(diff)
139+
self.update_pr(comment_text, args, create_new=True)
140+
else:
141+
print(
142+
f"Warning: {self.friendly_name}, {self.name} detected some issues with your code formatting..."
143+
)
99144
return False
100145
else:
101146
# The formatter failed but didn't output a diff (e.g. some sort of
@@ -118,7 +163,7 @@ def instructions(self) -> str:
118163
def should_include_extensionless_file(self, path: str) -> bool:
119164
return path.startswith("libcxx/include")
120165

121-
def filter_changed_files(self, changed_files: list[str]) -> list[str]:
166+
def filter_changed_files(self, changed_files: List[str]) -> List[str]:
122167
filtered_files = []
123168
for path in changed_files:
124169
_, ext = os.path.splitext(path)
@@ -128,32 +173,49 @@ def filter_changed_files(self, changed_files: list[str]) -> list[str]:
128173
filtered_files.append(path)
129174
return filtered_files
130175

131-
def format_run(
132-
self, changed_files: list[str], args: argparse.Namespace
133-
) -> str | None:
176+
@property
177+
def clang_fmt_path(self) -> str:
178+
if "CLANG_FORMAT_PATH" in os.environ:
179+
return os.environ["CLANG_FORMAT_PATH"]
180+
return "git-clang-format"
181+
182+
def has_tool(self) -> bool:
183+
cmd = [self.clang_fmt_path, "-h"]
184+
proc = None
185+
try:
186+
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
187+
except:
188+
return False
189+
return proc.returncode == 0
190+
191+
def format_run(self, changed_files: List[str], args: FormatArgs) -> Optional[str]:
134192
cpp_files = self.filter_changed_files(changed_files)
135193
if not cpp_files:
136194
return None
137-
cf_cmd = [
138-
"git-clang-format",
139-
"--diff",
140-
args.start_rev,
141-
args.end_rev,
142-
"--",
143-
] + cpp_files
144-
print(f"Running: {' '.join(cf_cmd)}")
195+
196+
cf_cmd = [self.clang_fmt_path, "--diff"]
197+
198+
if args.start_rev and args.end_rev:
199+
cf_cmd.append(args.start_rev)
200+
cf_cmd.append(args.end_rev)
201+
202+
cf_cmd.append("--")
203+
cf_cmd += cpp_files
204+
205+
if args.verbose:
206+
print(f"Running: {' '.join(cf_cmd)}")
145207
self.cf_cmd = cf_cmd
146-
proc = subprocess.run(cf_cmd, capture_output=True)
208+
proc = subprocess.run(cf_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
147209
sys.stdout.write(proc.stderr.decode("utf-8"))
148210

149211
if proc.returncode != 0:
150212
# formatting needed, or the command otherwise failed
151-
print(f"error: {self.name} exited with code {proc.returncode}")
152-
# Print the diff in the log so that it is viewable there
153-
print(proc.stdout.decode("utf-8"))
213+
if args.verbose:
214+
print(f"error: {self.name} exited with code {proc.returncode}")
215+
# Print the diff in the log so that it is viewable there
216+
print(proc.stdout.decode("utf-8"))
154217
return proc.stdout.decode("utf-8")
155218
else:
156-
sys.stdout.write(proc.stdout.decode("utf-8"))
157219
return None
158220

159221

@@ -165,7 +227,7 @@ class DarkerFormatHelper(FormatHelper):
165227
def instructions(self) -> str:
166228
return " ".join(self.darker_cmd)
167229

168-
def filter_changed_files(self, changed_files: list[str]) -> list[str]:
230+
def filter_changed_files(self, changed_files: List[str]) -> List[str]:
169231
filtered_files = []
170232
for path in changed_files:
171233
name, ext = os.path.splitext(path)
@@ -174,29 +236,48 @@ def filter_changed_files(self, changed_files: list[str]) -> list[str]:
174236

175237
return filtered_files
176238

177-
def format_run(
178-
self, changed_files: list[str], args: argparse.Namespace
179-
) -> str | None:
239+
@property
240+
def darker_fmt_path(self) -> str:
241+
if "DARKER_FORMAT_PATH" in os.environ:
242+
return os.environ["DARKER_FORMAT_PATH"]
243+
return "darker"
244+
245+
def has_tool(self) -> bool:
246+
cmd = [self.darker_fmt_path, "--version"]
247+
proc = None
248+
try:
249+
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
250+
except:
251+
return False
252+
return proc.returncode == 0
253+
254+
def format_run(self, changed_files: List[str], args: FormatArgs) -> Optional[str]:
180255
py_files = self.filter_changed_files(changed_files)
181256
if not py_files:
182257
return None
183258
darker_cmd = [
184-
"darker",
259+
self.darker_fmt_path,
185260
"--check",
186261
"--diff",
187-
"-r",
188-
f"{args.start_rev}...{args.end_rev}",
189-
] + py_files
190-
print(f"Running: {' '.join(darker_cmd)}")
262+
]
263+
if args.start_rev and args.end_rev:
264+
darker_cmd += ["-r", f"{args.start_rev}...{args.end_rev}"]
265+
darker_cmd += py_files
266+
if args.verbose:
267+
print(f"Running: {' '.join(darker_cmd)}")
191268
self.darker_cmd = darker_cmd
192-
proc = subprocess.run(darker_cmd, capture_output=True)
193-
sys.stdout.write(proc.stderr.decode("utf-8"))
269+
proc = subprocess.run(
270+
darker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
271+
)
272+
if args.verbose:
273+
sys.stdout.write(proc.stderr.decode("utf-8"))
194274

195275
if proc.returncode != 0:
196276
# formatting needed, or the command otherwise failed
197-
print(f"error: {self.name} exited with code {proc.returncode}")
198-
# Print the diff in the log so that it is viewable there
199-
print(proc.stdout.decode("utf-8"))
277+
if args.verbose:
278+
print(f"error: {self.name} exited with code {proc.returncode}")
279+
# Print the diff in the log so that it is viewable there
280+
print(proc.stdout.decode("utf-8"))
200281
return proc.stdout.decode("utf-8")
201282
else:
202283
sys.stdout.write(proc.stdout.decode("utf-8"))
@@ -205,7 +286,39 @@ def format_run(
205286

206287
ALL_FORMATTERS = (DarkerFormatHelper(), ClangFormatHelper())
207288

289+
290+
def hook_main():
291+
# fill out args
292+
args = FormatArgs()
293+
args.verbose = False
294+
295+
# find the changed files
296+
cmd = ["git", "diff", "--cached", "--name-only", "--diff-filter=d"]
297+
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
298+
output = proc.stdout.decode("utf-8")
299+
for line in output.splitlines():
300+
args.changed_files.append(line)
301+
302+
failed_fmts = []
303+
for fmt in ALL_FORMATTERS:
304+
if fmt.has_tool():
305+
if not fmt.run(args.changed_files, args):
306+
failed_fmts.append(fmt.name)
307+
else:
308+
print(f"Couldn't find {fmt.name}, can't check " + fmt.friendly_name.lower())
309+
310+
if len(failed_fmts) > 0:
311+
sys.exit(1)
312+
313+
sys.exit(0)
314+
315+
208316
if __name__ == "__main__":
317+
script_path = os.path.abspath(__file__)
318+
if ".git/hooks" in script_path:
319+
hook_main()
320+
sys.exit(0)
321+
209322
parser = argparse.ArgumentParser()
210323
parser.add_argument(
211324
"--token", type=str, required=True, help="GitHub authentiation token"
@@ -232,7 +345,7 @@ def format_run(
232345
help="Comma separated list of files that has been changed",
233346
)
234347

235-
args = parser.parse_args()
348+
args = FormatArgs(parser.parse_args())
236349

237350
changed_files = []
238351
if args.changed_files:

0 commit comments

Comments
 (0)