1
1
#!/usr/bin/env python3
2
2
#
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 -*--==#
4
4
#
5
5
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6
6
# See https://llvm.org/LICENSE.txt for license information.
7
7
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8
8
#
9
- # ==-------------------------------------------------------------------------==#
9
+ # ==-------------------------------------------------------------------------------------- ==#
10
10
11
11
import argparse
12
12
import os
13
13
import subprocess
14
14
import sys
15
- from functools import cached_property
15
+ from typing import List , Optional
16
16
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
19
54
20
55
21
56
class FormatHelper :
@@ -31,9 +66,10 @@ def comment_tag(self) -> str:
31
66
def instructions (self ) -> str :
32
67
raise NotImplementedError ()
33
68
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 ]:
37
73
raise NotImplementedError ()
38
74
39
75
def pr_comment_text_for_diff (self , diff : str ) -> str :
@@ -63,17 +99,18 @@ def pr_comment_text_for_diff(self, diff: str) -> str:
63
99
</details>
64
100
"""
65
101
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 :
69
105
for comment in pr .as_issue ().get_comments ():
70
106
if self .comment_tag in comment .body :
71
107
return comment
72
108
return None
73
109
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
+
77
114
repo = github .Github (args .token ).get_repo (args .repo )
78
115
pr = repo .get_issue (args .issue_number ).as_pull_request ()
79
116
@@ -85,17 +122,25 @@ def update_pr(
85
122
elif create_new :
86
123
pr .as_issue ().create_comment (comment_text )
87
124
88
- def run (self , changed_files : list [str ], args : argparse . Namespace ) -> bool :
125
+ def run (self , changed_files : List [str ], args : FormatArgs ) -> bool :
89
126
diff = self .format_run (changed_files , args )
127
+ should_update_gh = args .token is not None and args .repo is not None
128
+
90
129
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 )
95
135
return True
96
136
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
+ )
99
144
return False
100
145
else :
101
146
# The formatter failed but didn't output a diff (e.g. some sort of
@@ -118,7 +163,7 @@ def instructions(self) -> str:
118
163
def should_include_extensionless_file (self , path : str ) -> bool :
119
164
return path .startswith ("libcxx/include" )
120
165
121
- def filter_changed_files (self , changed_files : list [str ]) -> list [str ]:
166
+ def filter_changed_files (self , changed_files : List [str ]) -> List [str ]:
122
167
filtered_files = []
123
168
for path in changed_files :
124
169
_ , ext = os .path .splitext (path )
@@ -128,32 +173,49 @@ def filter_changed_files(self, changed_files: list[str]) -> list[str]:
128
173
filtered_files .append (path )
129
174
return filtered_files
130
175
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 ]:
134
192
cpp_files = self .filter_changed_files (changed_files )
135
193
if not cpp_files :
136
194
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 )} " )
145
207
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 )
147
209
sys .stdout .write (proc .stderr .decode ("utf-8" ))
148
210
149
211
if proc .returncode != 0 :
150
212
# 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" ))
154
217
return proc .stdout .decode ("utf-8" )
155
218
else :
156
- sys .stdout .write (proc .stdout .decode ("utf-8" ))
157
219
return None
158
220
159
221
@@ -165,7 +227,7 @@ class DarkerFormatHelper(FormatHelper):
165
227
def instructions (self ) -> str :
166
228
return " " .join (self .darker_cmd )
167
229
168
- def filter_changed_files (self , changed_files : list [str ]) -> list [str ]:
230
+ def filter_changed_files (self , changed_files : List [str ]) -> List [str ]:
169
231
filtered_files = []
170
232
for path in changed_files :
171
233
name , ext = os .path .splitext (path )
@@ -174,29 +236,48 @@ def filter_changed_files(self, changed_files: list[str]) -> list[str]:
174
236
175
237
return filtered_files
176
238
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 ]:
180
255
py_files = self .filter_changed_files (changed_files )
181
256
if not py_files :
182
257
return None
183
258
darker_cmd = [
184
- "darker" ,
259
+ self . darker_fmt_path ,
185
260
"--check" ,
186
261
"--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 )} " )
191
268
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" ))
194
274
195
275
if proc .returncode != 0 :
196
276
# 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" ))
200
281
return proc .stdout .decode ("utf-8" )
201
282
else :
202
283
sys .stdout .write (proc .stdout .decode ("utf-8" ))
@@ -205,7 +286,39 @@ def format_run(
205
286
206
287
ALL_FORMATTERS = (DarkerFormatHelper (), ClangFormatHelper ())
207
288
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
+
208
316
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
+
209
322
parser = argparse .ArgumentParser ()
210
323
parser .add_argument (
211
324
"--token" , type = str , required = True , help = "GitHub authentiation token"
@@ -232,7 +345,7 @@ def format_run(
232
345
help = "Comma separated list of files that has been changed" ,
233
346
)
234
347
235
- args = parser .parse_args ()
348
+ args = FormatArgs ( parser .parse_args () )
236
349
237
350
changed_files = []
238
351
if args .changed_files :
0 commit comments