acecalisto3 commited on
Commit
c7cea09
·
verified ·
1 Parent(s): c6a5c1a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +260 -261
app.py CHANGED
@@ -1,283 +1,282 @@
1
- import os
2
- import github
3
- from github import Github
4
- import requests
5
- from typing import List, Dict, Optional
6
  import logging
 
 
7
  from datetime import datetime
8
- import pytest
9
- from abc import ABC, abstractmethod
10
- import base64
11
- from concurrent.futures import ThreadPoolExecutor
12
- import re
13
- from dataclasses import dataclass
14
- from enum import Enum
15
-
16
- class IssueSeverity(Enum):
17
- CRITICAL = 5
18
- HIGH = 4
19
- MEDIUM = 3
20
- LOW = 2
21
- TRIVIAL = 1
22
-
23
- @dataclass
24
- class CodeContext:
25
- file_path: str
26
- content: str
27
- language: str
28
-
29
- class GitHubGuardianAngel:
30
- def __init__(self, github_token: str, ai_provider: AIProvider):
31
- self.gh = Github(github_token)
32
- self.ai = ai_provider
33
- self.logger = self._setup_logging()
34
- self.supported_extensions = {
35
- '.py': 'Python',
36
- '.js': 'JavaScript',
37
- '.ts': 'TypeScript',
38
- '.java': 'Java',
39
- '.cpp': 'C++',
40
- '.go': 'Go',
41
- '.rs': 'Rust'
 
 
 
 
 
 
 
 
42
  }
 
43
 
44
- def _setup_logging(self):
45
- logger = logging.getLogger('guardian_angel')
46
- logger.setLevel(logging.INFO)
47
- handler = logging.FileHandler('guardian_angel.log')
48
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
49
- handler.setFormatter(formatter)
50
- logger.addHandler(handler)
51
- return logger
52
-
53
- def _get_main_branch_content(self, repo) -> List[CodeContext]:
54
- """
55
- Retrieves and analyzes the content of the main branch
56
- """
57
  try:
58
- # Get default branch
59
- default_branch = repo.default_branch
60
- branch = repo.get_branch(default_branch)
61
- tree = repo.get_git_tree(branch.commit.sha, recursive=True)
62
-
63
- code_contexts = []
64
-
65
- def process_file(element):
66
- if element.type == 'blob':
67
- _, ext = os.path.splitext(element.path)
68
- if ext in self.supported_extensions:
69
- try:
70
- content = repo.get_contents(element.path).decoded_content.decode('utf-8')
71
- return CodeContext(
72
- file_path=element.path,
73
- content=content,
74
- language=self.supported_extensions[ext]
75
- )
76
- except Exception as e:
77
- self.logger.warning(f"Failed to process file {element.path}: {str(e)}")
78
- return None
79
-
80
- # Process files in parallel
81
- with ThreadPoolExecutor(max_workers=10) as executor:
82
- results = list(executor.map(process_file, tree.tree))
83
-
84
- code_contexts = [r for r in results if r is not None]
85
-
86
- return code_contexts
87
 
 
 
 
 
 
 
 
 
88
  except Exception as e:
89
- self.logger.error(f"Error getting main branch content: {str(e)}")
90
  raise
91
 
92
- def _determine_severity(self, issue, codebase_analysis) -> IssueSeverity:
93
- """
94
- Determines issue severity based on various factors
95
- """
96
- try:
97
- severity_indicators = {
98
- 'critical': ['crash', 'security', 'vulnerability', 'urgent', 'production down'],
99
- 'high': ['bug', 'error', 'failure', 'broken'],
100
- 'medium': ['enhancement', 'improvement', 'update needed'],
101
- 'low': ['minor', 'cosmetic', 'style', 'documentation'],
102
- 'trivial': ['typo', 'formatting']
103
- }
104
-
105
- # Check labels
106
- label_texts = [label.name.lower() for label in issue.labels]
107
-
108
- # Check title and body
109
- text_to_analyze = f"{issue.title.lower()} {issue.body.lower()}"
110
-
111
- # Calculate severity score
112
- severity_score = 0
113
-
114
- for severity, indicators in severity_indicators.items():
115
- for indicator in indicators:
116
- if indicator in text_to_analyze or any(indicator in label for label in label_texts):
117
- if severity == 'critical':
118
- severity_score = max(severity_score, 5)
119
- elif severity == 'high':
120
- severity_score = max(severity_score, 4)
121
- elif severity == 'medium':
122
- severity_score = max(severity_score, 3)
123
- elif severity == 'low':
124
- severity_score = max(severity_score, 2)
125
- else:
126
- severity_score = max(severity_score, 1)
127
-
128
- # Consider issue age
129
- age_days = (datetime.now() - issue.created_at).days
130
- if age_days > 30:
131
- severity_score += 1
132
- if age_days > 90:
133
- severity_score += 1
134
-
135
- # Map score to severity enum
136
- return IssueSeverity(min(severity_score, 5))
137
 
 
 
 
 
 
138
  except Exception as e:
139
- self.logger.error(f"Error determining severity: {str(e)}")
140
- return IssueSeverity.MEDIUM
141
 
142
- def _get_issue_context(self, repo, issue) -> str:
143
- """
144
- Gathers relevant context for the issue including related code and discussions
145
- """
146
- try:
147
- context_parts = []
148
-
149
- # Add issue details
150
- context_parts.append(f"Issue #{issue.number}: {issue.title}")
151
- context_parts.append(f"Description: {issue.body}")
152
-
153
- # Add labels
154
- context_parts.append(f"Labels: {', '.join([l.name for l in issue.labels])}")
155
-
156
- # Add related files (if mentioned in the issue)
157
- file_patterns = re.findall(r'`(.*?)`|\b\w+\.[a-zA-Z]+\b', issue.body)
158
- related_files = []
159
-
160
- for pattern in file_patterns:
161
- try:
162
- content = repo.get_contents(pattern)
163
- if isinstance(content, list):
164
- continue
165
- decoded_content = content.decoded_content.decode('utf-8')
166
- related_files.append(f"File: {pattern}\n```\n{decoded_content}\n```")
167
- except:
168
- continue
169
-
170
- if related_files:
171
- context_parts.append("Related Files:")
172
- context_parts.extend(related_files)
173
-
174
- # Add comments
175
- comments = issue.get_comments()
176
- if comments.totalCount > 0:
177
- context_parts.append("Relevant Comments:")
178
- for comment in comments[:5]: # Limit to last 5 comments
179
- context_parts.append(f"Comment by {comment.user.login}:\n{comment.body}")
180
-
181
- return "\n\n".join(context_parts)
182
 
 
 
 
 
183
  except Exception as e:
184
- self.logger.error(f"Error getting issue context: {str(e)}")
185
- return f"Issue #{issue.number}: {issue.title}\n{issue.body}"
186
 
187
- def _test_solution(self, solution: str, repo) -> Dict:
188
- """
189
- Tests the proposed solution using pytest
190
- """
191
  try:
192
- # Create temporary test directory
193
- test_dir = "temp_test_dir"
194
- os.makedirs(test_dir, exist_ok=True)
195
-
196
- # Extract code blocks from solution
197
- code_blocks = re.findall(r'```(?:python)?\n(.*?)```', solution, re.DOTALL)
198
-
199
- test_results = {
200
- 'status': 'untested',
201
- 'details': [],
202
- 'error': None
203
- }
204
-
205
- if not code_blocks:
206
- test_results['status'] = 'no_code_found'
207
- return test_results
208
-
209
- # Write code blocks to test files
210
- for i, code in enumerate(code_blocks):
211
- test_file = os.path.join(test_dir, f'test_solution_{i}.py')
212
- with open(test_file, 'w') as f:
213
- f.write(code)
214
-
215
- try:
216
- # Run pytest on the file
217
- test_output = pytest.main(['-v', test_file])
218
- test_results['details'].append({
219
- 'file': f'test_solution_{i}.py',
220
- 'status': 'passed' if test_output == 0 else 'failed',
221
- 'output': str(test_output)
222
- })
223
- except Exception as e:
224
- test_results['details'].append({
225
- 'file': f'test_solution_{i}.py',
226
- 'status': 'error',
227
- 'error': str(e)
228
- })
229
-
230
- # Determine overall status
231
- if any(d['status'] == 'error' for d in test_results['details']):
232
- test_results['status'] = 'error'
233
- elif any(d['status'] == 'failed' for d in test_results['details']):
234
- test_results['status'] = 'failed'
235
- else:
236
- test_results['status'] = 'passed'
237
-
238
- return test_results
239
-
240
  except Exception as e:
241
- self.logger.error(f"Error testing solution: {str(e)}")
242
- return {
243
- 'status': 'error',
244
- 'details': [],
245
- 'error': str(e)
246
- }
247
- finally:
248
- # Cleanup
249
- if os.path.exists(test_dir):
250
- import shutil
251
- shutil.rmtree(test_dir)
252
-
253
- def _comment_solution(self, issue, solution: str, test_results: Dict):
254
- """
255
- Posts a detailed solution comment on the issue
256
- """
257
- status_emoji = {
258
- 'passed': '✅',
259
- 'failed': '❌',
260
- 'error': '⚠️',
261
- 'untested': '⚪',
262
- 'no_code_found': '❓'
263
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
- comment = f"""
266
- ## 🔮 GitHub Guardian Angel Analysis
267
- ### Proposed Solution:
268
- {solution}
269
- Test Results {status_emoji.get(test_results['status'], '⚪')}
270
- Status: {test_results['status'].upper()}
 
 
 
 
271
 
272
- {"#### Test Details:" if test_results['details'] else ""} {"".join([f"- {d['file']}: {status_emoji.get(d['status'], '⚪')} {d['status'].upper()}\n" for d in test_results['details']])}
 
 
 
 
273
 
274
- {f"⚠️ Error: {test_results['error']}" if test_results.get('error') else ""}
 
275
 
276
- Implementation Steps:
277
- Review the proposed solution and test results
278
- Apply the changes in the code blocks above
279
- Run the provided tests to verify the fix
280
- If tests pass, commit and push the changes
281
- Close this issue with a reference to the fixing commit
282
- 💡 Please provide feedback on this solution. If you need any clarification or adjustments, let me know! """ try: issue.create_comment(comment) self.logger.info(f"Posted solution comment on issue #{issue.number}") except Exception as e: self.logger.error(f"Error posting comment: {str(e)}") raise
283
 
 
 
 
1
+ import sys
2
+ import shutil
 
 
 
3
  import logging
4
+ import time
5
+ import os
6
  from datetime import datetime
7
+ from typing import List, Dict, Any
8
+ import requests
9
+ import gradio as gr
10
+ import atexit
11
+ import subprocess
12
+ from urllib.parse import urlparse, quote
13
+ import webbrowser
14
+
15
+ # Constants
16
+ INPUT_DIRECTORY = 'input'
17
+ OUTPUT_DIRECTORY = 'output'
18
+ LOGS_DIRECTORY = 'logs'
19
+ RESOLUTIONS_DIRECTORY = 'resolutions'
20
+ REPOS_DIRECTORY = 'repos'
21
+
22
+ # Set up logging
23
+ def initialize_logger() -> logging.Logger:
24
+ log_file = f"{LOGS_DIRECTORY}/github_bot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
28
+ handlers=[
29
+ logging.FileHandler(log_file),
30
+ logging.StreamHandler()
31
+ ]
32
+ )
33
+ return logging.getLogger(__name__)
34
+
35
+ # Initialize environment and logger
36
+ def initialize_environment():
37
+ directories = [LOGS_DIRECTORY, RESOLUTIONS_DIRECTORY, REPOS_DIRECTORY, INPUT_DIRECTORY, OUTPUT_DIRECTORY]
38
+ for directory in directories:
39
+ os.makedirs(directory, exist_ok=True)
40
+
41
+ # GitHub API handler
42
+ class GitHubAPI:
43
+ def __init__(self, token: str, logger: logging.Logger):
44
+ self.token = token
45
+ self.logger = logger
46
+ self.headers = {
47
+ 'Authorization': f'token {token}',
48
+ 'Accept': 'application/vnd.github.v3+json'
49
  }
50
+ self.base_url = "https://api.github.com"
51
 
52
+ def _check_rate_limit(self) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
53
  try:
54
+ response = requests.get(f"{self.base_url}/rate_limit", headers=self.headers)
55
+ response.raise_for_status()
56
+ limits = response.json()
57
+ remaining = limits['resources']['core']['remaining']
58
+ reset_time = limits['resources']['core']['reset']
59
+
60
+ if remaining < 10:
61
+ wait_time = max(0, reset_time - int(time.time()))
62
+ if wait_time > 0:
63
+ self.logger.warning(f"Rate limit nearly exceeded. Waiting {wait_time} seconds before retrying...")
64
+ time.sleep(wait_time)
65
+ return False
66
+ return True
67
+ except requests.exceptions.RequestException as e:
68
+ self.logger.error(f"Error checking rate limit: {str(e)}. Retrying...")
69
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ def get_repository(self, owner: str, repo: str) -> Dict:
72
+ try:
73
+ response = requests.get(f"{self.base_url}/repos/{owner}/{repo}", headers=self.headers)
74
+ response.raise_for_status()
75
+ return response.json()
76
+ except requests.HTTPError as e:
77
+ self.logger.error(f"HTTP error getting repository info for {owner}/{repo}: {str(e)}. Please check the repository details.")
78
+ raise
79
  except Exception as e:
80
+ self.logger.error(f"Error getting repository info: {str(e)}")
81
  raise
82
 
83
+ def get_issues(self, owner: str, repo: str, state: str = 'open') -> List[Dict]:
84
+ if not self._check_rate_limit():
85
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ try:
88
+ response = requests.get(f"{self.base_url}/repos/{owner}/{repo}/issues", headers=self.headers, params={'state': state})
89
+ response.raise_for_status()
90
+ issues = response.json()
91
+ return [issue for issue in issues if 'pull_request' not in issue]
92
  except Exception as e:
93
+ self.logger.error(f"Error fetching issues for repository {owner}/{repo}: {str(e)}. Please verify the repository and token.")
94
+ return []
95
 
96
+ # GitHub Bot
97
+ class GitHubBot:
98
+ def __init__(self, logger: logging.Logger):
99
+ self.github_api = None
100
+ self.logger = logger
101
+
102
+ def initialize_api(self, token: str):
103
+ self.github_api = GitHubAPI(token, self.logger)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
+ def fetch_issues(self, token: str, owner: str, repo: str) -> List[Dict]:
106
+ try:
107
+ self.initialize_api(token)
108
+ return self.github_api.get_issues(owner, repo)
109
  except Exception as e:
110
+ self.logger.error(f"Error fetching issues for repository {owner}/{repo}: {str(e)}")
111
+ return []
112
 
113
+ def resolve_issue(self, token: str, owner: str, repo: str, issue_number: int, resolution: str, forked_repo: str) -> str:
 
 
 
114
  try:
115
+ self.initialize_api(token)
116
+ self.github_api.get_repository(owner, repo)
117
+
118
+ # Create resolution file
119
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
120
+ resolution_file = f"{RESOLUTIONS_DIRECTORY}/resolution_{issue_number}_{timestamp}.md"
121
+
122
+ with open(resolution_file, "w") as f:
123
+ f.write(f"# Resolution for Issue #{issue_number}\n\n{resolution}")
124
+
125
+ # Validate forked_repo before cloning
126
+ if not forked_repo:
127
+ raise ValueError("Forked repository URL cannot be empty.")
128
+
129
+ # Clone the forked repo
130
+ subprocess.run(['git', 'clone', forked_repo, '/tmp/' + forked_repo.split('/')[-1]], check=True)
131
+
132
+ # Change to the cloned directory
133
+ os.chdir('/tmp/' + forked_repo.split('/')[-1])
134
+
135
+ # Assuming manual intervention now
136
+ input("Apply the fix manually and stage the changes (press ENTER)? ")
137
+
138
+ # Commit and push the modifications
139
+ subprocess.run(['git', 'add', '.'], check=True)
140
+ subprocess.run(['git', 'commit', '-m', f"Resolved issue #{issue_number} ({quote(resolution)})"], check=True)
141
+ subprocess.run(['git', 'push', 'origin', 'HEAD'], check=True)
142
+
143
+ # Open Pull Request page
144
+ webbrowser.open(f'https://github.com/{forked_repo.split("/")[-1]}/compare/master...{owner}:{forked_repo.split("/")[-1]}_resolved_issue_{issue_number}')
145
+
146
+ return f"Resolution saved: {resolution_file}"
147
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  except Exception as e:
149
+ error_msg = f"Error resolving issue #{issue_number} in repository {owner}/{repo}: {str(e)}"
150
+ self.logger.error(error_msg)
151
+ return error_msg
152
+
153
+ def suggest_automated_fixes(self, issue_title: str) -> str:
154
+ if "missing README" in issue_title.lower():
155
+ return "Consider adding a README.md file to provide project documentation."
156
+ return "No automated fix available for this issue."
157
+
158
+ def handle_issue_selection(token, owner, repo, issue_number, resolution, forked_repo):
159
+ bot = GitHubBot(logger)
160
+ result = bot.resolve_issue(token, owner, repo, issue_number, resolution, forked_repo)
161
+ return result
162
+
163
+ def extract_info_from_url(url: str) -> Dict[str, Any]:
164
+ info = {}
165
+ try:
166
+ response = requests.get(url)
167
+ response.raise_for_status()
168
+ info['status_code'] = response.status_code
169
+ info['headers'] = dict(response.headers)
170
+ info['content'] = response.text[:500] # Limit content to first 500 characters for brevity
171
+
172
+ parsed_url = urlparse(url)
173
+ if 'github.com' in parsed_url.netloc:
174
+ parts = parsed_url.path.split('/')
175
+ if len(parts) > 2:
176
+ owner = parts[1]
177
+ repo = parts[2]
178
+ issues = bot.fetch_issues(github_token, owner, repo)
179
+ info['issues'] = issues
180
+ elif 'huggingface.co' in parsed_url.netloc:
181
+ # Add Hugging Face specific handling if needed
182
+ pass
183
+
184
+ except requests.HTTPError as e:
185
+ info['error'] = f"HTTP error: {str(e)}"
186
+ except Exception as e:
187
+ info['error'] = f"Error: {str(e)}"
188
+ return info
189
+
190
+ # Initialize GitHubBot globally
191
+ logger = initialize_logger() # Initialize logger before creating the bot
192
+ bot = GitHubBot(logger)
193
+
194
+ # Define missing functions with validation
195
+ def fetch_issues(token, repo_url):
196
+ try:
197
+ parts = repo_url.split('/')
198
+ if len(parts) < 2:
199
+ raise ValueError("Repository URL is not in the correct format. Expected format: 'owner/repo'.")
200
+
201
+ owner, repo = parts[-2], parts[-1]
202
+ issues = bot.fetch_issues(token, owner, repo)
203
+ return issues
204
+ except Exception as e:
205
+ return str(e)
206
+
207
+ def resolve_issue(token, repo_url, issue_number, resolution, forked_repo_url):
208
+ try:
209
+ parts = repo_url.split('/')
210
+ if len(parts) < 2:
211
+ raise ValueError("Repository URL is not in the correct format. Expected format: 'owner/repo'.")
212
+
213
+ owner, repo = parts[-2], parts[-1]
214
+ result = bot.resolve_issue(token, owner, repo, issue_number, resolution, forked_repo_url)
215
+ return result
216
+ except Exception as e:
217
+ return str(e)
218
+
219
+ def extract_info(url):
220
+ try:
221
+ info = extract_info_from_url(url)
222
+ return info
223
+ except Exception as e:
224
+ return str(e)
225
+
226
+ def create_gradio_interface():
227
+ with gr.Blocks() as demo:
228
+ gr.Markdown("# GitHub Issue Resolver")
229
+ gr.Markdown("This application allows you to fetch and resolve GitHub issues efficiently.")
230
+
231
+ with gr.Row():
232
+ token_input = gr.Textbox(label="GitHub Token", placeholder="Enter your GitHub token")
233
+ repo_url_input = gr.Textbox(label="Repository URL", placeholder="Enter the repository URL (owner/repo)")
234
+
235
+ with gr.Row():
236
+ issue_number_input = gr.Number(label="Issue Number", info="Enter the issue number")
237
+ resolution_input = gr.Textbox(label="Resolution", placeholder="Describe the resolution for the issue", lines=4)
238
+
239
+ forked_repo_input = gr.Textbox(label="Forked Repository URL", placeholder="Enter the forked repository URL")
240
+
241
+ submit_button = gr.Button("Resolve Issue")
242
+ result_output = gr.Textbox(label="Result", interactive=False)
243
+
244
+ def on_submit(token, repo_url, issue_number, resolution, forked_repo):
245
+ issues = fetch_issues(token, repo_url)
246
+ if issues:
247
+ automated_fix = bot.suggest_automated_fixes(issues[0]['title'])
248
+ resolution += f"\n\n**Automated Suggestion:** {automated_fix}"
249
+ result = resolve_issue(token, repo_url, issue_number, resolution, forked_repo)
250
+ return result
251
+ return "No issues found or an error occurred."
252
+
253
+ submit_button.click(on_submit, inputs=[token_input, repo_url_input, issue_number_input, resolution_input, forked_repo_input], outputs=result_output)
254
+
255
+ return demo
256
 
257
+ # Cleanup function
258
+ def cleanup():
259
+ try:
260
+ temp_dirs = [REPOS_DIRECTORY]
261
+ for dir_name in temp_dirs:
262
+ if os.path.exists(dir_name):
263
+ shutil.rmtree(dir_name)
264
+ logging.shutdown()
265
+ except Exception as e:
266
+ print(f"Error during cleanup: {str(e)}")
267
 
268
+ def main():
269
+ # Initialize environment and logger
270
+ initialize_environment()
271
+ global logger
272
+ logger = initialize_logger()
273
 
274
+ # Register cleanup handlers
275
+ atexit.register(cleanup)
276
 
277
+ # Create Gradio interface
278
+ demo = create_gradio_interface()
279
+ demo.launch()
 
 
 
 
280
 
281
+ if __name__ == "__main__":
282
+ main()