Files
cyclop/tools/github_tool.py
T

1297 lines
60 KiB
Python

# tools/github_tool.py
from .base_tool import BaseTool
from .metrics import metrics
import requests
import os
import base64
import logging
class GitHubTool(BaseTool):
def __init__(self):
self.base_url = "https://api.github.com"
self.token = os.environ.get("GITHUB_TOKEN")
self.headers = {
"Authorization": f"token {self.token}",
"Accept": "application/vnd.github.v3+json"
}
self.repo = os.environ.get("GITHUB_REPOSITORY")
self.current_branch = "main" # Default to main branch
# Set up logging
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.INFO)
# Create a file handler
file_handler = logging.FileHandler('github_tool.log')
file_handler.setLevel(logging.INFO)
# Create a console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# Create a formatting for the logs
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# Add the handlers to the logger
self.logger.addHandler(file_handler)
self.logger.addHandler(console_handler)
def clear(self):
if (self.current_branch != "main"):
self._set_current_branch("main")
pass
def get_functions(self):
return [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read a file from the repository",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path to the file in the repository"}
},
"required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "list_files",
"description": "List files in a directory of the repository",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path to the directory in the repository"}
},
"required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "search_code",
"description": "Search for code in the repository",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "create_branch",
"description": "Create a new branch in the repository",
"parameters": {
"type": "object",
"properties": {
"branch_name": {"type": "string", "description": "Name of the new branch"},
"base_branch": {"type": "string", "description": "Name of the base branch", "default": "main"}
},
"required": ["branch_name"]
}
}
},
{
"type": "function",
"function": {
"name": "commit_file",
"description": "Commit a file to a branch (not main)",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to the file in the repository"},
"commit_message": {"type": "string", "description": "Commit message"},
"content": {"type": "string", "description": "Content of the file"}
},
"required": ["file_path", "commit_message", "content"]
}
}
},
{
"type": "function",
"function": {
"name": "create_pull_request",
"description": "Create a pull request",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "Title of the pull request"},
"body": {"type": "string", "description": "Body of the pull request"},
"base": {"type": "string", "description": "The name of the branch you want the changes pulled into", "default": "main"}
},
"required": ["title", "body"]
}
}
},
{
"type": "function",
"function": {
"name": "get_commit_history",
"description": "Get commit history for a file",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to the file in the repository"},
"num_commits": {"type": "integer", "description": "Number of commits to retrieve", "default": 10}
},
"required": ["file_path"]
}
}
},
{
"type": "function",
"function": {
"name": "view_commit_details_for_file",
"description": "View commit history and details for a specific file, including commit messages.",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to the file in the repository"},
"num_commits": {"type": "integer", "description": "Number of commits to retrieve", "default": 10}
},
"required": ["file_path"]
}
}
},
{
"type": "function",
"function": {
"name": "get_branch_sha",
"description": "Get the SHA of the latest commit on a branch",
"parameters": {
"type": "object",
"properties": {
"branch": {"type": "string", "description": "Name of the branch"}
},
"required": ["branch"]
}
}
},
{
"type": "function",
"function": {
"name": "get_current_branch",
"description": "Get the name of the current branch",
"parameters": { "type": "object", "properties": {} }
}
},
{
"type": "function",
"function": {
"name": "set_current_branch",
"description": "Set the current branch",
"parameters": {
"type": "object",
"properties": {
"branch_name": {"type": "string", "description": "Name of the branch to set as current"}
},
"required": ["branch_name"]
}
}
},
{
"type": "function",
"function": {
"name": "get_file_at_commit",
"description": "Get the contents of a file at a specific commit",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to the file in the repository"},
"commit_sha": {"type": "string", "description": "SHA of the commit to retrieve the file from"}
},
"required": ["file_path", "commit_sha"]
}
}
},
{
"type": "function",
"function": {
"name": "list_branches",
"description": "List all branches in the repository",
"parameters": {
"type": "object",
"properties": {
"per_page": {"type": "integer", "description": "Number of branches to return per page (max 100)", "default": 100},
"all_pages": {"type": "boolean", "description": "Whether to fetch all pages of results", "default": True}
}
}
}
},
{
"type": "function",
"function": {
"name": "approve_pull_request",
"description": "Approve a pull request",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"}
},
"required": ["pull_number"]
}
}
},
{
"type": "function",
"function": {
"name": "close_pull_request",
"description": "Close a pull request",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"}
},
"required": ["pull_number"]
}
}
},
{
"type": "function",
"function": {
"name": "merge_pull_request",
"description": "Merge a pull request",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"},
"commit_title": {"type": "string", "description": "Title for the automatic commit message", "default": "Merge pull request"},
"commit_message": {"type": "string", "description": "Extra detail to append to automatic commit message", "default": ""},
"merge_method": {
"type": "string",
"description": "Merge method to use",
"enum": ["merge", "squash", "rebase"],
"default": "merge"
}
},
"required": ["pull_number"]
}
}
},
{
"type": "function",
"function": {
"name": "delete_branch",
"description": "Delete a branch",
"parameters": {
"type": "object",
"properties": {
"branch_name": {"type": "string", "description": "Name of the branch to delete"}
},
"required": ["branch_name"]
}
}
},
{
"type": "function",
"function": {
"name": "get_issue_details",
"description": "Get details of a specific issue",
"parameters": {
"type": "object",
"properties": {
"issue_number": {"type": "integer", "description": "The number of the issue"}
},
"required": ["issue_number"]
}
}
},
{
"type": "function",
"function": {
"name": "create_issue",
"description": "Create a new issue in the repository",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "Title of the issue"},
"body": {"type": "string", "description": "Body of the issue"},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Labels to apply to the issue"
}
},
"required": ["title", "body"]
}
}
},
{
"type": "function",
"function": {
"name": "list_issues",
"description": "List issues in the repository",
"parameters": {
"type": "object",
"properties": {
"state": {"type": "string", "enum": ["open", "closed", "all"], "default": "open", "description": "State of the issues to retrieve"},
"per_page": {"type": "integer", "default": 30, "description": "Number of issues to return per page"},
"page": {"type": "integer", "default": 1, "description": "Page number of the results to fetch"}
}
}
}
},
{
"type": "function",
"function": {
"name": "add_issue_comment",
"description": "Add a comment to an issue",
"parameters": {
"type": "object",
"properties": {
"issue_number": {"type": "integer", "description": "The number of the issue"},
"comment": {"type": "string", "description": "The comment to add to the issue"}
},
"required": ["issue_number", "comment"]
}
}
},
{
"type": "function",
"function": {
"name": "get_issue_comments",
"description": "Get comments for an issue",
"parameters": {
"type": "object",
"properties": {
"issue_number": {"type": "integer", "description": "The number of the issue"}
},
"required": ["issue_number"]
}
}
},
{
"type": "function",
"function": {
"name": "create_project_board",
"description": "Create a new project board",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name of the project board"},
"body": {"type": "string", "description": "Body of the project board"}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "create_project_column",
"description": "Create a new column in a project board",
"parameters": {
"type": "object",
"properties": {
"project_id": {"type": "integer", "description": "ID of the project board"},
"column_name": {"type": "string", "description": "Name of the column"}
},
"required": ["project_id", "column_name"]
}
}
},
{
"type": "function",
"function": {
"name": "create_project_card",
"description": "Create a new card in a project column",
"parameters": {
"type": "object",
"properties": {
"column_id": {"type": "integer", "description": "ID of the project column"},
"note": {"type": "string", "description": "Note for the project card"}
},
"required": ["column_id", "note"]
}
}
},
{
"type": "function",
"function": {
"name": "move_project_card",
"description": "Move a card to a new position",
"parameters": {
"type": "object",
"properties": {
"card_id": {"type": "integer", "description": "ID of the project card"},
"position": {"type": "string", "description": "New position of the card"},
"column_id": {"type": "integer", "description": "ID of the target column"}
},
"required": ["card_id", "position", "column_id"]
}
}
},
{
"type": "function",
"function": {
"name": "link_issue_to_project_card",
"description": "Link an issue or pull request to a project card",
"parameters": {
"type": "object",
"properties": {
"card_id": {"type": "integer", "description": "ID of the project card"},
"content_id": {"type": "integer", "description": "ID of the issue or pull request"},
"content_type": {"type": "string", "description": "Type of the content (Issue or PullRequest)"}
},
"required": ["card_id", "content_id", "content_type"]
}
}
},
{
"type": "function",
"function": {
"name": "list_project_boards",
"description": "List project boards associated with the repository",
"parameters": { "type": "object", "properties": {} }
}
},
{
"type": "function",
"function": {
"name": "view_project_board_items",
"description": "View items (columns and cards) in a specific project board",
"parameters": {
"type": "object",
"properties": {
"project_id": {"type": "integer", "description": "ID of the project board"}
},
"required": ["project_id"]
}
}
},
# New functions for PR review
{
"type": "function",
"function": {
"name": "get_pull_request_details",
"description": "Get detailed information about a pull request",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"}
},
"required": ["pull_number"]
}
}
},
{
"type": "function",
"function": {
"name": "get_pull_request_diff",
"description": "Get the diff of a pull request",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"}
},
"required": ["pull_number"]
}
}
},
{
"type": "function",
"function": {
"name": "get_pull_request_files",
"description": "Get a list of files changed in a pull request, with their patch details",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"}
},
"required": ["pull_number"]
}
}
},
{
"type": "function",
"function": {
"name": "create_pull_request_review_comment",
"description": "Add a comment to a specific line of a file in a pull request review",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"},
"body": {"type": "string", "description": "The text of the comment"},
"commit_id": {"type": "string", "description": "The SHA of the commit to comment on"},
"path": {"type": "string", "description": "The path to the file being commented on"},
"position": {"type": "integer", "description": "The line index in the diff to comment on (starting at 1)"},
"side": {"type": "string", "enum": ["LEFT", "RIGHT"], "description": "The side of the diff to comment on (LEFT for old file, RIGHT for new file)", "default": "RIGHT"},
"start_line": {"type": "integer", "description": "The start line of the diff if commenting on a range"},
"start_side": {"type": "string", "enum": ["LEFT", "RIGHT"], "description": "The side of the diff for the start line"}
},
"required": ["pull_number", "body", "commit_id", "path", "position"]
}
}
},
{
"type": "function",
"function": {
"name": "list_pull_request_review_comments",
"description": "List comments on a pull request review",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"}
},
"required": ["pull_number"]
}
}
},
{
"type": "function",
"function": {
"name": "submit_pull_request_review",
"description": "Submit a formal pull request review (APPROVE, REQUEST_CHANGES, COMMENT)",
"parameters": {
"type": "object",
"properties": {
"pull_number": {"type": "integer", "description": "The number of the pull request"},
"event": {"type": "string", "enum": ["APPROVE", "REQUEST_CHANGES", "COMMENT"], "description": "The type of review event"},
"body": {"type": "string", "description": "The body of the review (required for REQUEST_CHANGES, optional for others)"}
},
"required": ["pull_number", "event"]
}
}
}
]
@metrics.measure
def execute(self, function_name, **kwargs):
self.logger.info(f"Executing: {function_name}")
if function_name == "read_file":
return self._read_file(kwargs["path"])
elif function_name == "create_branch":
return self._create_branch(kwargs["branch_name"], kwargs.get("base_branch", "main"))
elif function_name == "commit_file":
return self._commit_file(kwargs["file_path"], kwargs["content"], kwargs["commit_message"])
elif function_name == "create_pull_request":
return self._create_pull_request(kwargs["title"], kwargs["body"], kwargs.get("base", "main"))
elif function_name == "list_files":
return self._list_files(kwargs["path"])
elif function_name == "search_code":
return self._search_code(kwargs["query"])
elif function_name == "get_commit_history":
return self._get_commit_history(kwargs["file_path"], kwargs.get("num_commits", 10))
elif function_name == "view_commit_details_for_file":
return self._view_commit_details_for_file(kwargs["file_path"], kwargs.get("num_commits", 10))
elif function_name == "get_current_branch":
return self._get_current_branch()
elif function_name == "set_current_branch":
return self._set_current_branch(kwargs["branch_name"])
elif function_name == "get_file_at_commit":
return self._get_file_at_commit(kwargs["file_path"], kwargs["commit_sha"])
elif function_name == "list_branches":
return self._list_branches(kwargs.get("per_page", 100), kwargs.get("all_pages", True))
elif function_name == "get_branch_sha":
return self._get_branch_sha(kwargs["branch"])
elif function_name == "approve_pull_request":
return self._approve_pull_request(kwargs["pull_number"])
elif function_name == "close_pull_request":
return self._close_pull_request(kwargs["pull_number"])
elif function_name == "merge_pull_request":
return self._merge_pull_request(kwargs["pull_number"], kwargs.get("commit_title", "Merge pull request"),
kwargs.get("commit_message", ""), kwargs.get("merge_method", "merge"))
elif function_name == "delete_branch":
return self._delete_branch(kwargs["branch_name"])
elif function_name == "get_issue_details":
return self._get_issue_details(kwargs["issue_number"])
elif function_name == "create_issue":
return self._create_issue(kwargs["title"], kwargs["body"], kwargs.get("labels", []))
elif function_name == "list_issues":
return self._list_issues(kwargs.get("state", "open"), kwargs.get("per_page", 30), kwargs.get("page", 1))
elif function_name == "add_issue_comment":
return self._add_issue_comment(kwargs["issue_number"], kwargs["comment"])
elif function_name == "get_issue_comments":
return self._get_issue_comments(kwargs["issue_number"])
elif function_name == "create_project_board":
return self._create_project_board(kwargs["name"], kwargs.get("body", ""))
elif function_name == "create_project_column":
return self._create_project_column(kwargs["project_id"], kwargs["column_name"])
elif function_name == "create_project_card":
return self._create_project_card(kwargs["column_id"], kwargs["note"])
elif function_name == "move_project_card":
return self._move_project_card(kwargs["card_id"], kwargs["position"], kwargs["column_id"])
elif function_name == "link_issue_to_project_card":
return self._link_issue_to_project_card(kwargs["card_id"], kwargs["content_id"], kwargs["content_type"])
elif function_name == "list_project_boards":
return self._list_project_boards()
elif function_name == "view_project_board_items":
return self._view_project_board_items(kwargs["project_id"])
# New function dispatching
elif function_name == "get_pull_request_details":
return self._get_pull_request_details(kwargs["pull_number"])
elif function_name == "get_pull_request_diff":
return self._get_pull_request_diff(kwargs["pull_number"])
elif function_name == "get_pull_request_files":
return self._get_pull_request_files(kwargs["pull_number"])
elif function_name == "create_pull_request_review_comment":
return self._create_pull_request_review_comment(kwargs["pull_number"], kwargs["body"], kwargs["commit_id"],
kwargs["path"], kwargs["position"], kwargs.get("side", "RIGHT"),
kwargs.get("start_line"), kwargs.get("start_side"))
elif function_name == "list_pull_request_review_comments":
return self._list_pull_request_review_comments(kwargs["pull_number"])
elif function_name == "submit_pull_request_review":
return self._submit_pull_request_review(kwargs["pull_number"], kwargs["event"], kwargs.get("body"))
else:
error_message = f"Unknown function: {function_name}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _read_file(self, path):
self.logger.info(f"Reading file: {path} from branch: {self.current_branch}")
url = f"{self.base_url}/repos/{self.repo}/contents/{path}"
response = requests.get(url, headers=self.headers, params={"ref": self.current_branch})
if response.status_code == 200:
content = response.json()["content"]
decoded_content = base64.b64decode(content).decode('utf-8')
self.logger.info(f"Successfully read file: {path}")
return decoded_content
else:
error_message = f"Error reading file: {response.status_code}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _create_branch(self, branch_name, base_branch):
self.logger.info(f"Creating branch: {branch_name} from base: {base_branch}")
url = f"{self.base_url}/repos/{self.repo}/git/refs"
response = requests.get(f"{url}/heads/{base_branch}", headers=self.headers)
if response.status_code != 200:
error_message = f"Error getting base branch: {response.status_code}"
self.logger.error(error_message)
return error_message
sha = response.json()["object"]["sha"]
data = {
"ref": f"refs/heads/{branch_name}",
"sha": sha
}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
self.current_branch = branch_name
success_message = f"Branch '{branch_name}' created successfully and set as current branch"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error creating branch: {response.status_code}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _commit_file(self, file_path, content, commit_message):
self.logger.info(f"Committing file: {file_path} to branch: {self.current_branch}")
if self.current_branch == "main":
error_message = "Cannot commit directly to main branch"
self.logger.error(error_message)
return error_message
url = f"{self.base_url}/repos/{self.repo}/contents/{file_path}"
self.logger.info("Checking if file already exists")
response = requests.get(url, headers=self.headers, params={"ref": self.current_branch})
data = {
"message": commit_message,
"content": base64.b64encode(content.encode()).decode(),
"branch": self.current_branch
}
if response.status_code == 200:
self.logger.info("File exists, updating")
file_sha = response.json()["sha"]
data["sha"] = file_sha
else:
self.logger.info("File does not exist, creating new file")
response = requests.put(url, headers=self.headers, json=data)
if response.status_code in [200, 201]:
success_message = f"File committed successfully to branch '{self.current_branch}'"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error committing file: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _create_pull_request(self, title, body, base):
self.logger.info(f"Creating pull request: {title} from {self.current_branch} to {base}")
url = f"{self.base_url}/repos/{self.repo}/pulls"
data = {
"title": title,
"body": body,
"head": self.current_branch,
"base": base
}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
success_message = f"Pull request created successfully: {response.json()['html_url']}"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error creating pull request: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _get_branch_sha(self, branch):
url = f"{self.base_url}/repos/{self.repo}/git/refs/heads/{branch}"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
return response.json()["object"]["sha"]
else:
return f"Error getting branch SHA: {response.status_code}"
@metrics.measure
def _list_files(self, path):
self.logger.info(f"Listing files in: {path} on branch: {self.current_branch}")
url = f"{self.base_url}/repos/{self.repo}/contents/{path}"
response = requests.get(url, headers=self.headers, params={"ref": self.current_branch})
if response.status_code == 200:
files = [{"type": "file", "name": item["name"]} for item in response.json() if item["type"] == "file"]
directories = [{"type": "directory", "name": item["name"]} for item in response.json() if item["type"] == "dir"]
self.logger.info(f"Successfully listed files and directories in {path}")
files.extend(directories)
return files
else:
error_message = f"Error listing files: {response.status_code}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _search_code(self, query):
self.logger.info(f"Searching code with query: {query}")
url = f"{self.base_url}/search/code"
params = {
"q": f"{query} repo:{self.repo}",
"per_page": 10
}
response = requests.get(url, headers=self.headers, params=params)
if response.status_code == 200:
results = [{"file": item["path"], "url": item["html_url"]} for item in response.json()["items"]]
self.logger.info(f"Successfully searched code. Found {len(results)} results.")
return results
else:
error_message = f"Error searching code: {response.status_code}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _get_commit_history(self, file_path, num_commits):
self.logger.info(f"Getting commit history for file: {file_path}, number of commits: {num_commits}")
url = f"{self.base_url}/repos/{self.repo}/commits"
params = {
"path": file_path,
"per_page": num_commits
}
response = requests.get(url, headers=self.headers, params=params)
if response.status_code == 200:
commits = [{"sha": commit["sha"], "message": commit["commit"]["message"], "date": commit["commit"]["author"]["date"]} for commit in response.json()]
self.logger.info(f"Successfully retrieved commit history. Found {len(commits)} commits.")
return commits
else:
error_message = f"Error getting commit history: {response.status_code}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _view_commit_details_for_file(self, file_path, num_commits):
self.logger.info(f"Viewing commit details for file: {file_path}, number of commits: {num_commits} (via _get_commit_history)")
return self._get_commit_history(file_path, num_commits)
@metrics.measure
def _get_current_branch(self):
self.logger.info(f"Getting current branch: {self.current_branch}")
return self.current_branch
@metrics.measure
def _set_current_branch(self, branch_name):
self.logger.info(f"Setting current branch from {self.current_branch} to {branch_name}")
self.current_branch = branch_name
return f"Current branch set to: {self.current_branch}"
@metrics.measure
def _get_file_at_commit(self, file_path, commit_sha):
self.logger.info(f"Getting file: {file_path} at commit: {commit_sha}")
url = f"{self.base_url}/repos/{self.repo}/contents/{file_path}"
response = requests.get(url, headers=self.headers, params={"ref": commit_sha})
if response.status_code == 200:
content = response.json()["content"]
decoded_content = base64.b64decode(content).decode('utf-8')
self.logger.info(f"Successfully retrieved file at commit")
return decoded_content
else:
error_message = f"Error reading file at commit: {response.status_code}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _list_branches(self, per_page=100, all_pages=True):
self.logger.info(f"Listing branches. Per page: {per_page}, All pages: {all_pages}")
url = f"{self.base_url}/repos/{self.repo}/branches"
params = {"per_page": min(per_page, 100)} # GitHub API max is 100 per page
all_branches = []
while url:
self.logger.info(f"Fetching branches from: {url}")
response = requests.get(url, headers=self.headers, params=params)
if response.status_code != 200:
error_message = f"Error listing branches: {response.status_code}"
self.logger.error(error_message)
return error_message
branches = [branch["name"] for branch in response.json()]
all_branches.extend(branches)
self.logger.info(f"Fetched {len(branches)} branches")
if not all_pages:
break
# Check if there's a next page
url = response.links.get('next', {}).get('url')
if url:
params = {} # Remove per_page for subsequent requests
self.logger.info(f"Successfully listed all branches. Total: {len(all_branches)}")
return all_branches
@metrics.measure
def _approve_pull_request(self, pull_number):
self.logger.info(f"Approving pull request: {pull_number}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}/reviews"
data = {"event": "APPROVE"}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 200:
success_message = f"Pull request {pull_number} approved successfully"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error approving pull request: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _close_pull_request(self, pull_number):
self.logger.info(f"Closing pull request: {pull_number}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}"
data = {"state": "closed"}
response = requests.patch(url, headers=self.headers, json=data)
if response.status_code == 200:
success_message = f"Pull request {pull_number} closed successfully"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error closing pull request: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _merge_pull_request(self, pull_number, commit_title, commit_message, merge_method):
self.logger.info(f"Merging pull request: {pull_number}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}/merge"
data = {"commit_title": commit_title, "commit_message": commit_message, "merge_method": merge_method}
response = requests.put(url, headers=self.headers, json=data)
if response.status_code == 200:
success_message = f"Pull request {pull_number} merged successfully"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error merging pull request: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _delete_branch(self, branch_name):
self.logger.info(f"Deleting branch: {branch_name}")
url = f"{self.base_url}/repos/{self.repo}/git/refs/heads/{branch_name}"
response = requests.delete(url, headers=self.headers)
if response.status_code == 204:
success_message = f"Branch {branch_name} deleted successfully"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error deleting branch: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _get_issue_details(self, issue_number):
self.logger.info(f"Getting details for issue: {issue_number}")
url = f"{self.base_url}/repos/{self.repo}/issues/{issue_number}"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
issue_data = response.json()
issue_details = {
"number": issue_data["number"],
"title": issue_data["title"],
"state": issue_data["state"],
"body": issue_data["body"],
"created_at": issue_data["created_at"],
"updated_at": issue_data["updated_at"],
"labels": [label["name"] for label in issue_data["labels"]],
"assignees": [assignee["login"] for assignee in issue_data["assignees"]],
"comments": issue_data["comments"]
}
self.logger.info(f"Successfully retrieved details for issue {issue_number}")
return issue_details
else:
error_message = f"Error getting issue details: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _create_issue(self, title, body, labels=None):
self.logger.info(f"Creating issue: {title}")
url = f"{self.base_url}/repos/{self.repo}/issues"
data = {
"title": title,
"body": body
}
if labels:
data["labels"] = labels
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
issue = response.json()
success_message = f"Issue created successfully: {issue['html_url']}"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error creating issue: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _list_issues(self, state="open", per_page=30, page=1):
self.logger.info(f"Listing issues. State: {state}, Per page: {per_page}, Page: {page}")
url = f"{self.base_url}/repos/{self.repo}/issues"
params = {
"state": state,
"per_page": per_page,
"page": page
}
response = requests.get(url, headers=self.headers, params=params)
if response.status_code == 200:
issues = [{
"number": issue["number"],
"title": issue["title"],
"state": issue["state"],
"created_at": issue["created_at"],
"url": issue["html_url"]
} for issue in response.json()]
self.logger.info(f"Successfully listed issues. Found {len(issues)} issues.")
return issues
else:
error_message = f"Error listing issues: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _add_issue_comment(self, issue_number, comment):
self.logger.info(f"Adding comment to issue: {issue_number}")
url = f"{self.base_url}/repos/{self.repo}/issues/{issue_number}/comments"
data = {"body": comment}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
comment_data = response.json()
success_message = f"Comment added successfully to issue {issue_number}: {comment_data['html_url']}"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error adding comment to issue: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _get_issue_comments(self, issue_number):
self.logger.info(f"Getting comments for issue: {issue_number}")
url = f"{self.base_url}/repos/{self.repo}/issues/{issue_number}/comments"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
comments = [{
"id": comment["id"],
"user": comment["user"]["login"],
"body": comment["body"],
"created_at": comment["created_at"],
"updated_at": comment["updated_at"]
} for comment in response.json()]
self.logger.info(f"Successfully retrieved comments for issue {issue_number}. Found {len(comments)} comments.")
return comments
else:
error_message = f"Error getting issue comments: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _create_project_board(self, name, body=None):
url = f"{self.base_url}/repos/{self.repo}/projects"
data = {"name": name, "body": body}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
project = response.json()
success_message = f"Project board '{name}' created successfully."
self.logger.info(success_message)
return {
"status": "success",
"status_code": response.status_code,
"message": success_message,
"data": project
}
else:
error_message = f"Error creating project board: {response.status_code}"
self.logger.error(error_message)
return {
"status": "error",
"status_code": response.status_code,
"message": error_message,
"response": response.text
}
@metrics.measure
def _create_project_column(self, project_id, column_name):
url = f"{self.base_url}/projects/{project_id}/columns"
data = {"name": column_name}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
column = response.json()
success_message = f"Column '{column_name}' created successfully in project {project_id}."
self.logger.info(success_message)
return {
"status": "success",
"status_code": response.status_code,
"message": success_message,
"data": column
}
else:
error_message = f"Error creating project column: {response.status_code}"
self.logger.error(error_message)
return {
"status": "error",
"status_code": response.status_code,
"message": error_message,
"response": response.text
}
@metrics.measure
def _create_project_card(self, column_id, note):
url = f"{self.base_url}/projects/columns/{column_id}/cards"
data = {"note": note}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
card = response.json()
success_message = f"Card created successfully in column {column_id}."
self.logger.info(success_message)
return {
"status": "success",
"status_code": response.status_code,
"message": success_message,
"data": card
}
else:
error_message = f"Error creating project card: {response.status_code}"
self.logger.error(error_message)
return {
"status": "error",
"status_code": response.status_code,
"message": error_message,
"response": response.text
}
@metrics.measure
def _move_project_card(self, card_id, position, column_id):
url = f"{self.base_url}/projects/columns/cards/{card_id}/moves"
data = {"position": position, "column_id": column_id}
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
success_message = f"Card {card_id} moved successfully."
self.logger.info(success_message)
return {
"status": "success",
"status_code": response.status_code,
"message": success_message
}
else:
error_message = f"Error moving project card: {response.status_code}"
self.logger.error(error_message)
return {
"status": "error",
"status_code": response.status_code,
"message": error_message,
"response": response.text
}
@metrics.measure
def _link_issue_to_project_card(self, card_id, content_id, content_type):
url = f"{self.base_url}/projects/columns/cards/{card_id}"
data = {"content_id": content_id, "content_type": content_type}
response = requests.patch(url, headers=self.headers, json=data)
if response.status_code == 200:
success_message = f"Issue/PR linked to card {card_id} successfully."
self.logger.info(success_message)
return {
"status": "success",
"status_code": response.status_code,
"message": success_message
}
else:
error_message = f"Error linking issue/PR to project card: {response.status_code}"
self.logger.error(error_message)
return {
"status": "error",
"status_code": response.status_code,
"message": error_message,
"response": response.text
}
@metrics.measure
def _list_project_boards(self):
self.logger.info("Fetching project boards...")
url = f"{self.base_url}/repos/{self.repo}/projects"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
boards = response.json()
self.logger.info(f"Successfully fetched {len(boards)} project boards.")
return boards
else:
error_message = f"Error fetching project boards: {response.status_code}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _view_project_board_items(self, project_id):
self.logger.info(f"Fetching items for project board ID: {project_id}...")
columns_url = f"{self.base_url}/projects/{project_id}/columns"
columns_response = requests.get(columns_url, headers=self.headers)
if columns_response.status_code == 200:
columns = columns_response.json()
items = []
for column in columns:
column_id = column["id"]
column_name = column["name"]
cards_url = f"{self.base_url}/projects/columns/{column_id}/cards"
cards_response = requests.get(cards_url, headers=self.headers)
if cards_response.status_code == 200:
cards = cards_response.json()
items.append({"column": column_name, "cards": cards})
else:
self.logger.error(f"Error fetching cards for column {column_id}: {cards_response.status_code}")
items.append({"column": column_name, "cards": "Error fetching cards"})
self.logger.info(f"Successfully fetched items for project board ID: {project_id}.")
return items
else:
error_message = f"Error fetching columns for project board: {columns_response.status_code}"
self.logger.error(error_message)
return error_message
# New functions for PR review capabilities
@metrics.measure
def _get_pull_request_details(self, pull_number):
self.logger.info(f"Getting details for pull request: {pull_number}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
self.logger.info(f"Successfully retrieved details for PR {pull_number}")
return response.json()
else:
error_message = f"Error getting pull request details: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _get_pull_request_diff(self, pull_number):
self.logger.info(f"Getting diff for pull request: {pull_number}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}"
headers = self.headers.copy()
headers["Accept"] = "application/vnd.github.v3.diff"
response = requests.get(url, headers=headers)
if response.status_code == 200:
self.logger.info(f"Successfully retrieved diff for PR {pull_number}")
return response.text
else:
error_message = f"Error getting pull request diff: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _get_pull_request_files(self, pull_number):
self.logger.info(f"Getting files for pull request: {pull_number}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}/files"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
self.logger.info(f"Successfully retrieved files for PR {pull_number}")
return response.json()
else:
error_message = f"Error getting pull request files: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _create_pull_request_review_comment(self, pull_number, body, commit_id, path, position, side="RIGHT", start_line=None, start_side=None):
self.logger.info(f"Adding review comment to PR {pull_number} on file {path} at position {position}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}/comments"
data = {
"body": body,
"commit_id": commit_id,
"path": path,
"position": position,
"side": side
}
if start_line is not None:
data["start_line"] = start_line
if start_side is not None:
data["start_side"] = start_side
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 201:
success_message = f"Comment added to PR {pull_number} successfully: {response.json()['html_url']}"
self.logger.info(success_message)
return success_message
else:
error_message = f"Error creating pull request review comment: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _list_pull_request_review_comments(self, pull_number):
self.logger.info(f"Listing review comments for pull request: {pull_number}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}/comments"
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
self.logger.info(f"Successfully retrieved review comments for PR {pull_number}")
return response.json()
else:
error_message = f"Error listing pull request review comments: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message
@metrics.measure
def _submit_pull_request_review(self, pull_number, event, body=None):
self.logger.info(f"Submitting review for pull request {pull_number} with event: {event}")
url = f"{self.base_url}/repos/{self.repo}/pulls/{pull_number}/reviews"
data = {"event": event}
if body:
data["body"] = body
response = requests.post(url, headers=self.headers, json=data)
if response.status_code == 200:
success_message = f"Review submitted for PR {pull_number} successfully."
self.logger.info(success_message)
return success_message
else:
error_message = f"Error submitting pull request review: {response.status_code}\nResponse: {response.text}"
self.logger.error(error_message)
return error_message