mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-02 11:50:37 +08:00
Merge pull request #255 from pzarfos/issue_138_codecommit_describe
Enhancement of AWS CodeCommit support in PR-Agent
This commit is contained in:
@ -309,7 +309,9 @@ Example IAM permissions to that user to allow access to CodeCommit:
|
||||
"codecommit:Get*",
|
||||
"codecommit:List*",
|
||||
"codecommit:PostComment*",
|
||||
"codecommit:PutCommentReaction"
|
||||
"codecommit:PutCommentReaction",
|
||||
"codecommit:UpdatePullRequestDescription",
|
||||
"codecommit:UpdatePullRequestTitle"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull
|
||||
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| | ⮑ Inline review | :white_check_mark: | :white_check_mark: | | |
|
||||
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| | Auto-Description | :white_check_mark: | :white_check_mark: | | |
|
||||
| | Auto-Description | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
|
||||
| | Improve Code | :white_check_mark: | :white_check_mark: | | |
|
||||
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | | |
|
||||
| | Reflect and Review | :white_check_mark: | | | |
|
||||
|
@ -64,7 +64,7 @@ class CodeCommitClient:
|
||||
"""
|
||||
Get the differences between two commits in CodeCommit.
|
||||
|
||||
Parameters:
|
||||
Args:
|
||||
- repo_name: Name of the repository
|
||||
- destination_commit: Commit hash you want to merge into (the "before" hash) (usually on the main or master branch)
|
||||
- source_commit: Commit hash of the code you are adding (the "after" branch)
|
||||
@ -73,8 +73,8 @@ class CodeCommitClient:
|
||||
- List of CodeCommitDifferencesResponse objects
|
||||
|
||||
Boto3 Documentation:
|
||||
aws codecommit get-differences
|
||||
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_differences.html
|
||||
- aws codecommit get-differences
|
||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_differences.html
|
||||
"""
|
||||
if self.boto_client is None:
|
||||
self._connect_boto_client()
|
||||
@ -101,7 +101,7 @@ class CodeCommitClient:
|
||||
"""
|
||||
Retrieve a file from CodeCommit.
|
||||
|
||||
Parameters:
|
||||
Args:
|
||||
- repo_name: Name of the repository
|
||||
- file_path: Path to the file you are retrieving
|
||||
- sha_hash: Commit hash of the file you are retrieving
|
||||
@ -110,8 +110,8 @@ class CodeCommitClient:
|
||||
- File contents
|
||||
|
||||
Boto3 Documentation:
|
||||
aws codecommit get_file
|
||||
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_file.html
|
||||
- aws codecommit get_file
|
||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_file.html
|
||||
"""
|
||||
if not file_path:
|
||||
return ""
|
||||
@ -137,15 +137,15 @@ class CodeCommitClient:
|
||||
"""
|
||||
Get a information about a CodeCommit PR.
|
||||
|
||||
Parameters:
|
||||
Args:
|
||||
- pr_number: The PR number you are requesting
|
||||
|
||||
Returns:
|
||||
- CodeCommitPullRequestResponse object
|
||||
|
||||
Boto3 Documentation:
|
||||
aws codecommit get_pull_request
|
||||
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_pull_request.html
|
||||
- aws codecommit get_pull_request
|
||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_pull_request.html
|
||||
"""
|
||||
if self.boto_client is None:
|
||||
self._connect_boto_client()
|
||||
@ -164,11 +164,48 @@ class CodeCommitClient:
|
||||
|
||||
return CodeCommitPullRequestResponse(response.get("pullRequest", {}))
|
||||
|
||||
def publish_description(self, pr_number: int, pr_title: str, pr_body: str):
|
||||
"""
|
||||
Set the title and description on a pull request
|
||||
|
||||
Args:
|
||||
- pr_number: the AWS CodeCommit pull request number
|
||||
- pr_title: title of the pull request
|
||||
- pr_body: body of the pull request
|
||||
|
||||
Returns:
|
||||
- None
|
||||
|
||||
Boto3 Documentation:
|
||||
- aws codecommit update_pull_request_title
|
||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/update_pull_request_title.html
|
||||
- aws codecommit update_pull_request_description
|
||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/update_pull_request_description.html
|
||||
"""
|
||||
if self.boto_client is None:
|
||||
self._connect_boto_client()
|
||||
|
||||
try:
|
||||
self.boto_client.update_pull_request_title(pullRequestId=str(pr_number), title=pr_title)
|
||||
self.boto_client.update_pull_request_description(pullRequestId=str(pr_number), description=pr_body)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException':
|
||||
raise ValueError(f"PR number does not exist: {pr_number}") from e
|
||||
if e.response["Error"]["Code"] == 'InvalidTitleException':
|
||||
raise ValueError(f"Invalid title for PR number: {pr_number}") from e
|
||||
if e.response["Error"]["Code"] == 'InvalidDescriptionException':
|
||||
raise ValueError(f"Invalid description for PR number: {pr_number}") from e
|
||||
if e.response["Error"]["Code"] == 'PullRequestAlreadyClosedException':
|
||||
raise ValueError(f"PR is already closed: PR number: {pr_number}") from e
|
||||
raise ValueError(f"Boto3 client error calling publish_description") from e
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error calling publish_description") from e
|
||||
|
||||
def publish_comment(self, repo_name: str, pr_number: int, destination_commit: str, source_commit: str, comment: str):
|
||||
"""
|
||||
Publish a comment to a pull request
|
||||
|
||||
Parameters:
|
||||
Args:
|
||||
- repo_name: name of the repository
|
||||
- pr_number: number of the pull request
|
||||
- destination_commit: The commit hash you want to merge into (the "before" hash) (usually on the main or master branch)
|
||||
@ -179,8 +216,8 @@ class CodeCommitClient:
|
||||
- None
|
||||
|
||||
Boto3 Documentation:
|
||||
aws codecommit post_comment_for_pull_request
|
||||
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_pull_request.html
|
||||
- aws codecommit post_comment_for_pull_request
|
||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_pull_request.html
|
||||
"""
|
||||
if self.boto_client is None:
|
||||
self._connect_boto_client()
|
||||
|
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections import Counter
|
||||
from typing import List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
@ -153,17 +154,27 @@ class CodeCommitProvider(GitProvider):
|
||||
return self.diff_files
|
||||
|
||||
def publish_description(self, pr_title: str, pr_body: str):
|
||||
return "" # not implemented yet
|
||||
try:
|
||||
self.codecommit_client.publish_description(
|
||||
pr_number=self.pr_num,
|
||||
pr_title=pr_title,
|
||||
pr_body=CodeCommitProvider._add_additional_newlines(pr_body),
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError(f"CodeCommit Cannot publish description for PR: {self.pr_num}") from e
|
||||
|
||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||
if is_temporary:
|
||||
logging.info(pr_comment)
|
||||
return
|
||||
|
||||
pr_comment = CodeCommitProvider._remove_markdown_html(pr_comment)
|
||||
pr_comment = CodeCommitProvider._add_additional_newlines(pr_comment)
|
||||
|
||||
try:
|
||||
self.codecommit_client.publish_comment(
|
||||
repo_name=self.repo_name,
|
||||
pr_number=str(self.pr_num),
|
||||
pr_number=self.pr_num,
|
||||
destination_commit=self.pr.destination_commit,
|
||||
source_commit=self.pr.source_commit,
|
||||
comment=pr_comment,
|
||||
@ -200,7 +211,7 @@ class CodeCommitProvider(GitProvider):
|
||||
Returns a dictionary of languages, containing the percentage of each language used in the PR.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR.
|
||||
- dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR.
|
||||
"""
|
||||
commit_files = self.get_files()
|
||||
filenames = [ item.filename for item in commit_files ]
|
||||
@ -251,11 +262,20 @@ class CodeCommitProvider(GitProvider):
|
||||
|
||||
@staticmethod
|
||||
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
|
||||
"""
|
||||
Parse the CodeCommit PR URL and return the repository name and PR number.
|
||||
|
||||
Args:
|
||||
- pr_url: the full AWS CodeCommit pull request URL
|
||||
|
||||
Returns:
|
||||
- Tuple[str, int]: A tuple containing the repository name and PR number.
|
||||
"""
|
||||
# Example PR URL:
|
||||
# https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/__MY_REPO__/pull-requests/123456"
|
||||
parsed_url = urlparse(pr_url)
|
||||
|
||||
if "us-east-1.console.aws.amazon.com" not in parsed_url.netloc:
|
||||
if not CodeCommitProvider._is_valid_codecommit_hostname(parsed_url.netloc):
|
||||
raise ValueError(f"The provided URL is not a valid CodeCommit URL: {pr_url}")
|
||||
|
||||
path_parts = parsed_url.path.strip("/").split("/")
|
||||
@ -278,6 +298,22 @@ class CodeCommitProvider(GitProvider):
|
||||
|
||||
return repo_name, pr_number
|
||||
|
||||
@staticmethod
|
||||
def _is_valid_codecommit_hostname(hostname: str) -> bool:
|
||||
"""
|
||||
Check if the provided hostname is a valid AWS CodeCommit hostname.
|
||||
|
||||
This is not an exhaustive check of AWS region names,
|
||||
but instead uses a regex to check for matching AWS region patterns.
|
||||
|
||||
Args:
|
||||
- hostname: the hostname to check
|
||||
|
||||
Returns:
|
||||
- bool: True if the hostname is valid, False otherwise.
|
||||
"""
|
||||
return re.match(r"^[a-z]{2}-(gov-)?[a-z]+-\d\.console\.aws\.amazon\.com$", hostname) is not None
|
||||
|
||||
def _get_pr(self):
|
||||
response = self.codecommit_client.get_pr(self.pr_num)
|
||||
|
||||
@ -306,13 +342,52 @@ class CodeCommitProvider(GitProvider):
|
||||
return "" # not implemented yet
|
||||
|
||||
@staticmethod
|
||||
def _get_edit_type(codecommit_change_type):
|
||||
def _add_additional_newlines(body: str) -> str:
|
||||
"""
|
||||
Replace single newlines in a PR body with double newlines.
|
||||
|
||||
CodeCommit Markdown does not seem to render as well as GitHub Markdown,
|
||||
so we add additional newlines to the PR body to make it more readable in CodeCommit.
|
||||
|
||||
Args:
|
||||
- body: the PR body
|
||||
|
||||
Returns:
|
||||
- str: the PR body with the double newlines added
|
||||
"""
|
||||
return re.sub(r'(?<!\n)\n(?!\n)', '\n\n', body)
|
||||
|
||||
@staticmethod
|
||||
def _remove_markdown_html(comment: str) -> str:
|
||||
"""
|
||||
Remove the HTML tags from a PR comment.
|
||||
|
||||
CodeCommit Markdown does not seem to render as well as GitHub Markdown,
|
||||
so we remove the HTML tags from the PR comment to make it more readable in CodeCommit.
|
||||
|
||||
Args:
|
||||
- comment: the PR comment
|
||||
|
||||
Returns:
|
||||
- str: the PR comment with the HTML tags removed
|
||||
"""
|
||||
comment = comment.replace("<details>", "")
|
||||
comment = comment.replace("</details>", "")
|
||||
comment = comment.replace("<summary>", "")
|
||||
comment = comment.replace("</summary>", "")
|
||||
return comment
|
||||
|
||||
@staticmethod
|
||||
def _get_edit_type(codecommit_change_type: str):
|
||||
"""
|
||||
Convert the CodeCommit change type string to the EDIT_TYPE enum.
|
||||
The CodeCommit change type string is returned from the get_differences SDK method.
|
||||
|
||||
Args:
|
||||
- codecommit_change_type: the CodeCommit change type string
|
||||
|
||||
Returns:
|
||||
An EDIT_TYPE enum representing the modified, added, deleted, or renamed file in the PR diff.
|
||||
- An EDIT_TYPE enum representing the modified, added, deleted, or renamed file in the PR diff.
|
||||
"""
|
||||
t = codecommit_change_type.upper()
|
||||
edit_type = None
|
||||
@ -333,6 +408,12 @@ class CodeCommitProvider(GitProvider):
|
||||
The returned extensions will include the dot "." prefix,
|
||||
to accommodate for the dots in the existing language_extension_map settings.
|
||||
Filenames with no extension will return an empty string for the extension.
|
||||
|
||||
Args:
|
||||
- filenames: a list of filenames
|
||||
|
||||
Returns:
|
||||
- list: A list of file extensions, including the dot "." prefix.
|
||||
"""
|
||||
extensions = []
|
||||
for filename in filenames:
|
||||
@ -349,6 +430,12 @@ class CodeCommitProvider(GitProvider):
|
||||
Return a dictionary containing the programming language name (as the key),
|
||||
and the percentage that language is used (as the value),
|
||||
given a list of file extensions.
|
||||
|
||||
Args:
|
||||
- extensions: a list of file extensions
|
||||
|
||||
Returns:
|
||||
- dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR.
|
||||
"""
|
||||
total_files = len(extensions)
|
||||
if total_files == 0:
|
||||
|
@ -26,11 +26,48 @@ class TestCodeCommitFile:
|
||||
|
||||
class TestCodeCommitProvider:
|
||||
def test_parse_pr_url(self):
|
||||
# Test that the _parse_pr_url() function can extract the repo name and PR number from a CodeCommit URL
|
||||
url = "https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/my_test_repo/pull-requests/321"
|
||||
repo_name, pr_number = CodeCommitProvider._parse_pr_url(url)
|
||||
assert repo_name == "my_test_repo"
|
||||
assert pr_number == 321
|
||||
|
||||
def test_is_valid_codecommit_hostname(self):
|
||||
# Test the various AWS regions
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("af-south-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-east-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-2.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-3.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-south-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-south-2.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-2.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-3.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-4.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ca-central-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-central-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-central-2.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-north-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-south-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-south-2.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-2.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-3.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("il-central-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("me-central-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("me-south-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("sa-east-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-east-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-east-2.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-gov-east-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-gov-west-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-west-1.console.aws.amazon.com")
|
||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-west-2.console.aws.amazon.com")
|
||||
# Test non-AWS regions
|
||||
assert not CodeCommitProvider._is_valid_codecommit_hostname("no-such-region.console.aws.amazon.com")
|
||||
assert not CodeCommitProvider._is_valid_codecommit_hostname("console.aws.amazon.com")
|
||||
|
||||
# Test that an error is raised when an invalid CodeCommit URL is provided to the set_pr() method of the CodeCommitProvider class.
|
||||
# Generated by CodiumAI
|
||||
def test_invalid_codecommit_url(self):
|
||||
@ -106,6 +143,7 @@ class TestCodeCommitProvider:
|
||||
assert percentages == {}
|
||||
|
||||
def test_get_edit_type(self):
|
||||
# Test that the _get_edit_type() function can convert a CodeCommit letter to an EDIT_TYPE enum
|
||||
assert CodeCommitProvider._get_edit_type("A") == EDIT_TYPE.ADDED
|
||||
assert CodeCommitProvider._get_edit_type("D") == EDIT_TYPE.DELETED
|
||||
assert CodeCommitProvider._get_edit_type("M") == EDIT_TYPE.MODIFIED
|
||||
@ -117,3 +155,18 @@ class TestCodeCommitProvider:
|
||||
assert CodeCommitProvider._get_edit_type("r") == EDIT_TYPE.RENAMED
|
||||
|
||||
assert CodeCommitProvider._get_edit_type("X") is None
|
||||
|
||||
def test_add_additional_newlines(self):
|
||||
# a short string to test adding double newlines
|
||||
input = "abc\ndef\n\n___\nghi\njkl\nmno\n\npqr\n"
|
||||
expect = "abc\n\ndef\n\n___\n\nghi\n\njkl\n\nmno\n\npqr\n\n"
|
||||
assert CodeCommitProvider._add_additional_newlines(input) == expect
|
||||
# a test example from a real PR
|
||||
input = "## PR Type:\nEnhancement\n\n___\n## PR Description:\nThis PR introduces a new feature to the script, allowing users to filter servers by name.\n\n___\n## PR Main Files Walkthrough:\n`foo`: The foo script has been updated to include a new command line option `-f` or `--filter`.\n`bar`: The bar script has been updated to list stopped servers.\n"
|
||||
expect = "## PR Type:\n\nEnhancement\n\n___\n\n## PR Description:\n\nThis PR introduces a new feature to the script, allowing users to filter servers by name.\n\n___\n\n## PR Main Files Walkthrough:\n\n`foo`: The foo script has been updated to include a new command line option `-f` or `--filter`.\n\n`bar`: The bar script has been updated to list stopped servers.\n\n"
|
||||
assert CodeCommitProvider._add_additional_newlines(input) == expect
|
||||
|
||||
def test_remove_markdown_html(self):
|
||||
input = "## PR Feedback\n<details><summary>Code feedback:</summary>\nfile foo\n</summary>\n"
|
||||
expect = "## PR Feedback\nCode feedback:\nfile foo\n\n"
|
||||
assert CodeCommitProvider._remove_markdown_html(input) == expect
|
Reference in New Issue
Block a user