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