1+ import os
2+ import requests
3+ import json
4+ import re
5+ from github import Github
6+ from collections import defaultdict
7+ from concurrent .futures import ThreadPoolExecutor , as_completed
8+
9+ def get_changed_files (pr ):
10+ changed_files = []
11+ for file in pr .get_files ():
12+ if file .filename .endswith ('.java' ):
13+ changed_files .append ({
14+ 'filename' : file .filename ,
15+ 'patch' : file .patch ,
16+ 'status' : file .status ,
17+ })
18+ return changed_files
19+
20+ def get_file_content (repo , file_path , ref ):
21+ return repo .get_contents (file_path , ref = ref ).decoded_content .decode ('utf-8' )
22+
23+ def search_file (repo , file , changed_files , ref ):
24+ if file .type == 'file' and file .name .endswith ('.java' ):
25+ content = get_file_content (repo , file .path , ref )
26+ related = set ()
27+ for changed_file in changed_files :
28+ changed_name = os .path .splitext (os .path .basename (changed_file ['filename' ]))[0 ]
29+ if re .search (r'\b' + re .escape (changed_name ) + r'\b' , content ):
30+ related .add (changed_file ['filename' ])
31+ return file .path , related
32+ return None , set ()
33+
34+ def find_related_files (repo , changed_files , ref ):
35+ related_files = defaultdict (set )
36+ all_files = repo .get_contents ('' , ref = ref )
37+ dirs_to_process = [file for file in all_files if file .type == 'dir' ]
38+
39+ with ThreadPoolExecutor (max_workers = 10 ) as executor :
40+ future_to_file = {executor .submit (search_file , repo , file , changed_files , ref ): file for file in all_files if file .type == 'file' }
41+
42+ while dirs_to_process :
43+ dir_files = repo .get_contents (dirs_to_process .pop ().path , ref = ref )
44+ dirs_to_process .extend ([file for file in dir_files if file .type == 'dir' ])
45+ future_to_file .update ({executor .submit (search_file , repo , file , changed_files , ref ): file for file in dir_files if file .type == 'file' })
46+
47+ for future in as_completed (future_to_file ):
48+ file_path , related = future .result ()
49+ if related :
50+ for changed_file in related :
51+ related_files [changed_file ].add (file_path )
52+
53+ return related_files
54+
55+
56+ def call_claude_api (changes , related_files ):
57+ url = "https://api.anthropic.com/v1/messages"
58+ headers = {
59+ "Content-Type" : "application/json" ,
60+ "x-api-key" : os .environ ['CLAUDE_API_KEY' ],
61+ "anthropic-version" : "2023-06-01"
62+ }
63+
64+ system_content = (
65+ "경험 많은 시니어 개발자로서, 다음 변경사항들에 대해 전체적이고 간결한 코드 리뷰를 수행해주세요.\n \n "
66+ "리뷰 지침:\n "
67+ "1. 모든 변경사항을 종합적으로 검토하고, 가장 중요한 문제점이나 개선사항에만 집중하세요.\n "
68+ "2. 파일별로 개별 리뷰를 하지 말고, 전체 변경사항에 대한 통합된 리뷰를 제공하세요.\n "
69+ "3. 각 주요 이슈에 대해 간단한 설명과 구체적인 개선 제안을 제시하세요.\n "
70+ "4. 개선 제안에는 실제 코드 예시를 포함하세요. 단, 코드 예시는 제공한 코드와 연관된 코드여야 합니다. \n "
71+ "5. 사소한 스타일 문제나 개인적 선호도는 무시하세요.\n "
72+ "6. 심각한 버그, 성능 문제, 또는 보안 취약점이 있는 경우에만 언급하세요.\n "
73+ "7. 전체 리뷰는 간결하게 유지하세요.\n "
74+ "8. 변경된 부분만 집중하여 리뷰하고, 이미 개선된 코드를 다시 지적하지 마세요.\n "
75+ "9. 기존에 이미 개선된 사항(예: 중복 코드 제거를 위한 함수 생성)을 인식하고 이를 긍정적으로 언급하세요.\n "
76+ "10. 변경된 파일과 관련된 다른 파일들에 미칠 수 있는 영향을 분석하세요.\n \n "
77+ "리뷰 형식:\n "
78+ "- 개선된 사항: [이미 개선된 부분에 대한 긍정적 언급]\n "
79+ "- 주요 이슈 (있는 경우에만):\n "
80+ " 1. [문제 설명]\n "
81+ " - 제안: [개선 방안 설명]\n "
82+ " ```java\n "
83+ " // 수정된 코드 예시\n "
84+ " ```\n "
85+ " 2. ...\n "
86+ "- 관련 파일에 대한 영향 분석:\n "
87+ " [변경된 파일과 관련된 다른 파일들에 미칠 수 있는 잠재적 영향 설명]\n "
88+ "- 전반적인 의견: [1-2문장으로 요약]\n \n "
89+ "변경된 파일들:\n "
90+ )
91+
92+ for file_info in changes :
93+ system_content += f"- { file_info ['filename' ]} ({ file_info ['status' ]} )\n "
94+
95+ system_content += "\n 변경 내용:\n "
96+ for file_info in changes :
97+ system_content += f"파일: { file_info ['filename' ]} \n 전체 내용:\n { file_info ['full_content' ]} \n \n 변경된 부분:\n { file_info ['patch' ]} \n \n "
98+
99+ system_content += "\n 관련된 파일들:\n "
100+ for changed_file , related in related_files .items ():
101+ system_content += f"- { changed_file } 에 영향을 받을 수 있는 파일들:\n "
102+ for related_file in related :
103+ system_content += f" - { related_file } \n "
104+
105+ payload = {
106+ "model" : "claude-3-5-sonnet-20240620" ,
107+ "max_tokens" : 2000 ,
108+ "system" : system_content ,
109+ "messages" : [
110+ {
111+ "role" : "user" ,
112+ "content" : [
113+ {
114+ "type" : "text" ,
115+ "text" : "제공된 모든 변경사항에 대해 통합된, 간결하고 핵심적인 코드 리뷰를 제공해주세요. 가장 중요한 이슈에만 집중하고, 각 개선 제안에는 구체적인 코드 예시를 포함해주세요. 변경된 부분만 집중하여 리뷰하고, 이미 개선된 코드를 다시 지적하지 마세요. 또한, 변경된 파일과 관련된 다른 파일들에 미칠 수 있는 잠재적 영향을 분석해주세요."
116+ }
117+ ]
118+ }
119+ ]
120+ }
121+
122+ response = requests .post (url , headers = headers , json = payload )
123+ if response .status_code == 200 :
124+ return response .json ()['content' ][0 ]['text' ]
125+ else :
126+ return f"Error: API returned status code { response .status_code } "
127+
128+ def main ():
129+ g = Github (os .environ ['GITHUB_TOKEN' ])
130+ repo = g .get_repo (os .environ ['GITHUB_REPOSITORY' ])
131+ pr_number = int (os .environ ['PR_NUMBER' ])
132+ pr = repo .get_pull (pr_number )
133+
134+ changed_files = get_changed_files (pr )
135+ changes = []
136+
137+ for file_info in changed_files :
138+ full_content = get_file_content (repo , file_info ['filename' ], pr .head .sha )
139+ file_info ['full_content' ] = full_content
140+ changes .append (file_info )
141+
142+ related_files = find_related_files (repo , changed_files , pr .head .sha )
143+ review = call_claude_api (changes , related_files )
144+
145+ pr .create_issue_comment (f"Claude의 전체 변경사항 및 관련 파일에 대한 리뷰:\n \n { review } " )
146+
147+ if __name__ == "__main__" :
148+ main ()
0 commit comments