1
+ import argparse
2
+ from github import Github
3
+ import os
4
+
5
+ # Input variables from Github action
6
+ GITHUB_TOKEN = os .getenv ('GITHUB_TOKEN' )
7
+ PR_NUM = int (os .getenv ('PR_NUMBER' ))
8
+ WORK_DIR = os .getenv ('GITHUB_WORKSPACE' )
9
+ REPO_NAME = os .getenv ('GITHUB_REPOSITORY' )
10
+ SHA = os .getenv ('GITHUB_SHA' )
11
+ COMMENT_TITLE = os .getenv ('COMMENT_TITLE' )
12
+ ONLY_PR_CHANGES = os .getenv ('REPORT_PR_CHANGES_ONLY' )
13
+
14
+ # Max characters per comment - 65536
15
+ # Make some room for HTML tags and error message
16
+ MAX_CHAR_COUNT_REACHED = '!Maximum character count per GitHub comment has been reached! Not all warnings/errors has been parsed!'
17
+ COMMENT_MAX_SIZE = 65000
18
+ current_comment_length = 0
19
+
20
+ def is_part_of_pr_changes (file_path , issue_file_line , files_changed_in_pr ):
21
+ if ONLY_PR_CHANGES == "false" :
22
+ return True
23
+
24
+ file_name = file_path [file_path .rfind ('/' )+ 1 :]
25
+ print (f"Looking for issue found in file={ file_name } ..." )
26
+ for file , (status , lines_changed_for_file ) in files_changed_in_pr .items ():
27
+ print (f"Changed file by this PR { file } with status { status } and changed lines { lines_changed_for_file } " )
28
+ if file == file_name :
29
+ if status == "added" :
30
+ return True
31
+
32
+ for (start , end ) in lines_changed_for_file :
33
+ if issue_file_line >= start and issue_file_line <= end :
34
+ return True
35
+
36
+ return False
37
+
38
+ def get_lines_changed_from_patch (patch ):
39
+ lines_changed = []
40
+ lines = patch .split ('\n ' )
41
+
42
+ for line in lines :
43
+ # Example line @@ -43,6 +48,8 @@
44
+ # ------------ ^
45
+ if line .startswith ("@@" ):
46
+ # Example line @@ -43,6 +48,8 @@
47
+ # ----------------------^
48
+ idx_beg = line .index ("+" )
49
+
50
+ # Example line @@ -43,6 +48,8 @@
51
+ # ^--^
52
+ idx_end = line [idx_beg :].index ("," )
53
+ line_begin = int (line [idx_beg + 1 : idx_beg + idx_end ])
54
+
55
+ idx_beg = idx_beg + idx_end
56
+ idx_end = line [idx_beg + 1 : ].index ("@@" )
57
+
58
+ num_lines = int (line [idx_beg + 1 : idx_beg + idx_end ])
59
+
60
+ lines_changed .append ((line_begin , line_begin + num_lines ))
61
+
62
+ return lines_changed
63
+
64
+ def setup_changed_files ():
65
+ files_changed = dict ()
66
+
67
+ g = Github (GITHUB_TOKEN )
68
+ repo = g .get_repo (REPO_NAME )
69
+ pull_request = repo .get_pull (PR_NUM )
70
+ num_changed_files = pull_request .changed_files
71
+ print (f"Changed files { num_changed_files } " )
72
+ files = pull_request .get_files ()
73
+ for file in files :
74
+ # additions # blob_url # changes # contents_url # deletions # filename
75
+ # patch # previous_filename # raw_url # sha # status
76
+ # print(f"File: additions={file.additions} blob_url={file.blob_url} changes={file.changes} contents_url={file.contents_url}"\
77
+ # f"deletions={file.deletions} filename={file.filename} patch={file.patch} previous_filename={file.previous_filename}"\
78
+ # f"raw_url={file.raw_url} sha={file.sha} status={file.status} ")
79
+
80
+ if file .patch is not None :
81
+ lines_changed_for_file = get_lines_changed_from_patch (file .patch )
82
+ files_changed [file .filename ] = (file .status , lines_changed_for_file )
83
+
84
+ return files_changed
85
+
86
+ def check_for_char_limit (incoming_line ):
87
+ global current_comment_length
88
+ return (current_comment_length + len (incoming_line )) <= COMMENT_MAX_SIZE
89
+
90
+ def get_file_line_end (file , file_line_start ):
91
+ num_lines = sum (1 for line in open (WORK_DIR + file ))
92
+ return min (file_line_start + 5 , num_lines )
93
+
94
+ def create_comment_for_output (tool_output , prefix , files_changed_in_pr ):
95
+ issues_found = 0
96
+ global current_comment_length
97
+ output_string = ''
98
+ for line in tool_output :
99
+ if line .startswith (prefix ):
100
+ line = line .replace (prefix , "" )
101
+ file_path_end_idx = line .index (':' )
102
+ file_path = line [:file_path_end_idx ]
103
+ line = line [file_path_end_idx + 1 :]
104
+ file_line_start = int (line [:line .index (':' )])
105
+ file_line_end = get_file_line_end (file_path , file_line_start )
106
+ description = f"\n ```diff\n !Line: { file_line_start } - { line [line .index (' ' )+ 1 :]} ``` \n "
107
+
108
+ new_line = f'\n \n https://github.com/{ REPO_NAME } /blob/{ SHA } { file_path } #L{ file_line_start } -L{ file_line_end } { description } <br>\n '
109
+
110
+ if is_part_of_pr_changes (file_path , file_line_start , files_changed_in_pr ):
111
+ if check_for_char_limit (new_line ):
112
+ output_string += new_line
113
+ current_comment_length += len (new_line )
114
+ issues_found += 1
115
+ else :
116
+ current_comment_length = COMMENT_MAX_SIZE
117
+ return output_string , issues_found
118
+
119
+ return output_string , issues_found
120
+
121
+ def read_files_and_parse_results (files_changed_in_pr ):
122
+ parser = argparse .ArgumentParser ()
123
+ parser .add_argument ('-cc' , '--cppcheck' , help = 'Output file name for cppcheck' , required = True )
124
+ cppcheck_file_name = parser .parse_args ().cppcheck
125
+
126
+ cppcheck_content = ''
127
+ with open (cppcheck_file_name , 'r' ) as file :
128
+ cppcheck_content = file .readlines ()
129
+
130
+ line_prefix = f'{ WORK_DIR } '
131
+
132
+ cppcheck_comment , cppcheck_issues_found = create_comment_for_output (cppcheck_content , line_prefix , files_changed_in_pr )
133
+
134
+ return cppcheck_comment , cppcheck_issues_found
135
+
136
+ def prepare_comment_body (cppcheck_comment , cppcheck_issues_found ):
137
+
138
+ if cppcheck_issues_found == 0 :
139
+ full_comment_body = f'## <p align="center"><b> :white_check_mark: { COMMENT_TITLE } - no issues found! :white_check_mark: </b></p>'
140
+ else :
141
+ full_comment_body = f'## <p align="center"><b> :zap: { COMMENT_TITLE } :zap: </b></p> \n \n '
142
+
143
+ if len (cppcheck_comment ) > 0 :
144
+ full_comment_body += f'<details> <summary> <b> :red_circle: Cppcheck found' \
145
+ f' { cppcheck_issues_found } { "issues" if cppcheck_issues_found > 1 else "issue" } ! Click here to see details. </b> </summary> <br>' \
146
+ f'{ cppcheck_comment } </details><br>\n '
147
+
148
+ if current_comment_length == COMMENT_MAX_SIZE :
149
+ full_comment_body += f'\n ```diff\n { MAX_CHAR_COUNT_REACHED } \n ```'
150
+
151
+ print (f'Repo={ REPO_NAME } pr_num={ PR_NUM } comment_title={ COMMENT_TITLE } ' )
152
+
153
+ return full_comment_body
154
+
155
+ def create_or_edit_comment (comment_body ):
156
+ g = Github (GITHUB_TOKEN )
157
+ repo = g .get_repo (REPO_NAME )
158
+ pr = repo .get_pull (PR_NUM )
159
+
160
+ comments = pr .get_issue_comments ()
161
+ found_id = - 1
162
+ comment_to_edit = None
163
+ for comment in comments :
164
+ if (comment .user .login == 'github-actions[bot]' ) and (COMMENT_TITLE in comment .body ):
165
+ found_id = comment .id
166
+ comment_to_edit = comment
167
+ break
168
+
169
+ if found_id != - 1 :
170
+ comment_to_edit .edit (body = comment_body )
171
+ else :
172
+ pr .create_issue_comment (body = comment_body )
173
+
174
+
175
+ if __name__ == "__main__" :
176
+ files_changed_in_pr = setup_changed_files ()
177
+ cppcheck_comment , cppcheck_issues_found = read_files_and_parse_results (files_changed_in_pr )
178
+ comment_body = prepare_comment_body (cppcheck_comment , cppcheck_issues_found )
179
+ create_or_edit_comment (comment_body )
0 commit comments