Merge pull request #255 from pzarfos/issue_138_codecommit_describe

Enhancement of AWS CodeCommit support in PR-Agent
This commit is contained in:
mrT23
2023-08-30 20:30:50 +03:00
committed by GitHub
5 changed files with 199 additions and 20 deletions

View File

@ -309,7 +309,9 @@ Example IAM permissions to that user to allow access to CodeCommit:
"codecommit:Get*", "codecommit:Get*",
"codecommit:List*", "codecommit:List*",
"codecommit:PostComment*", "codecommit:PostComment*",
"codecommit:PutCommentReaction" "codecommit:PutCommentReaction",
"codecommit:UpdatePullRequestDescription",
"codecommit:UpdatePullRequestTitle"
], ],
"Resource": "*" "Resource": "*"
} }

View File

@ -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: | | TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Inline review | :white_check_mark: | :white_check_mark: | | | | | ⮑ Inline review | :white_check_mark: | :white_check_mark: | | |
| | Ask | :white_check_mark: | :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: | | | | | Improve Code | :white_check_mark: | :white_check_mark: | | |
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | | | | | ⮑ Extended | :white_check_mark: | :white_check_mark: | | |
| | Reflect and Review | :white_check_mark: | | | | | | Reflect and Review | :white_check_mark: | | | |

View File

@ -64,7 +64,7 @@ class CodeCommitClient:
""" """
Get the differences between two commits in CodeCommit. Get the differences between two commits in CodeCommit.
Parameters: Args:
- repo_name: Name of the repository - 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) - 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) - source_commit: Commit hash of the code you are adding (the "after" branch)
@ -73,8 +73,8 @@ class CodeCommitClient:
- List of CodeCommitDifferencesResponse objects - List of CodeCommitDifferencesResponse objects
Boto3 Documentation: Boto3 Documentation:
aws codecommit get-differences - aws codecommit get-differences
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_differences.html - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_differences.html
""" """
if self.boto_client is None: if self.boto_client is None:
self._connect_boto_client() self._connect_boto_client()
@ -101,7 +101,7 @@ class CodeCommitClient:
""" """
Retrieve a file from CodeCommit. Retrieve a file from CodeCommit.
Parameters: Args:
- repo_name: Name of the repository - repo_name: Name of the repository
- file_path: Path to the file you are retrieving - file_path: Path to the file you are retrieving
- sha_hash: Commit hash of the file you are retrieving - sha_hash: Commit hash of the file you are retrieving
@ -110,8 +110,8 @@ class CodeCommitClient:
- File contents - File contents
Boto3 Documentation: Boto3 Documentation:
aws codecommit get_file - aws codecommit get_file
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_file.html - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_file.html
""" """
if not file_path: if not file_path:
return "" return ""
@ -137,15 +137,15 @@ class CodeCommitClient:
""" """
Get a information about a CodeCommit PR. Get a information about a CodeCommit PR.
Parameters: Args:
- pr_number: The PR number you are requesting - pr_number: The PR number you are requesting
Returns: Returns:
- CodeCommitPullRequestResponse object - CodeCommitPullRequestResponse object
Boto3 Documentation: Boto3 Documentation:
aws codecommit get_pull_request - aws codecommit get_pull_request
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_pull_request.html - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_pull_request.html
""" """
if self.boto_client is None: if self.boto_client is None:
self._connect_boto_client() self._connect_boto_client()
@ -164,11 +164,48 @@ class CodeCommitClient:
return CodeCommitPullRequestResponse(response.get("pullRequest", {})) 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): 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 Publish a comment to a pull request
Parameters: Args:
- repo_name: name of the repository - repo_name: name of the repository
- pr_number: number of the pull request - 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) - 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 - None
Boto3 Documentation: Boto3 Documentation:
aws codecommit post_comment_for_pull_request - 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 - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_pull_request.html
""" """
if self.boto_client is None: if self.boto_client is None:
self._connect_boto_client() self._connect_boto_client()

View File

@ -1,5 +1,6 @@
import logging import logging
import os import os
import re
from collections import Counter from collections import Counter
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
@ -153,17 +154,27 @@ class CodeCommitProvider(GitProvider):
return self.diff_files return self.diff_files
def publish_description(self, pr_title: str, pr_body: str): 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): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if is_temporary: if is_temporary:
logging.info(pr_comment) logging.info(pr_comment)
return return
pr_comment = CodeCommitProvider._remove_markdown_html(pr_comment)
pr_comment = CodeCommitProvider._add_additional_newlines(pr_comment)
try: try:
self.codecommit_client.publish_comment( self.codecommit_client.publish_comment(
repo_name=self.repo_name, repo_name=self.repo_name,
pr_number=str(self.pr_num), pr_number=self.pr_num,
destination_commit=self.pr.destination_commit, destination_commit=self.pr.destination_commit,
source_commit=self.pr.source_commit, source_commit=self.pr.source_commit,
comment=pr_comment, 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 a dictionary of languages, containing the percentage of each language used in the PR.
Returns: 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() commit_files = self.get_files()
filenames = [ item.filename for item in commit_files ] filenames = [ item.filename for item in commit_files ]
@ -251,11 +262,20 @@ class CodeCommitProvider(GitProvider):
@staticmethod @staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]: 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: # Example PR URL:
# https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/__MY_REPO__/pull-requests/123456" # https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/__MY_REPO__/pull-requests/123456"
parsed_url = urlparse(pr_url) 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}") raise ValueError(f"The provided URL is not a valid CodeCommit URL: {pr_url}")
path_parts = parsed_url.path.strip("/").split("/") path_parts = parsed_url.path.strip("/").split("/")
@ -278,6 +298,22 @@ class CodeCommitProvider(GitProvider):
return repo_name, pr_number 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): def _get_pr(self):
response = self.codecommit_client.get_pr(self.pr_num) response = self.codecommit_client.get_pr(self.pr_num)
@ -306,13 +342,52 @@ class CodeCommitProvider(GitProvider):
return "" # not implemented yet return "" # not implemented yet
@staticmethod @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. Convert the CodeCommit change type string to the EDIT_TYPE enum.
The CodeCommit change type string is returned from the get_differences SDK method. The CodeCommit change type string is returned from the get_differences SDK method.
Args:
- codecommit_change_type: the CodeCommit change type string
Returns: 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() t = codecommit_change_type.upper()
edit_type = None edit_type = None
@ -333,6 +408,12 @@ class CodeCommitProvider(GitProvider):
The returned extensions will include the dot "." prefix, The returned extensions will include the dot "." prefix,
to accommodate for the dots in the existing language_extension_map settings. to accommodate for the dots in the existing language_extension_map settings.
Filenames with no extension will return an empty string for the extension. 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 = [] extensions = []
for filename in filenames: for filename in filenames:
@ -349,6 +430,12 @@ class CodeCommitProvider(GitProvider):
Return a dictionary containing the programming language name (as the key), Return a dictionary containing the programming language name (as the key),
and the percentage that language is used (as the value), and the percentage that language is used (as the value),
given a list of file extensions. 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) total_files = len(extensions)
if total_files == 0: if total_files == 0:

View File

@ -26,11 +26,48 @@ class TestCodeCommitFile:
class TestCodeCommitProvider: class TestCodeCommitProvider:
def test_parse_pr_url(self): 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" 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) repo_name, pr_number = CodeCommitProvider._parse_pr_url(url)
assert repo_name == "my_test_repo" assert repo_name == "my_test_repo"
assert pr_number == 321 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. # 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 # Generated by CodiumAI
def test_invalid_codecommit_url(self): def test_invalid_codecommit_url(self):
@ -106,6 +143,7 @@ class TestCodeCommitProvider:
assert percentages == {} assert percentages == {}
def test_get_edit_type(self): 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("A") == EDIT_TYPE.ADDED
assert CodeCommitProvider._get_edit_type("D") == EDIT_TYPE.DELETED assert CodeCommitProvider._get_edit_type("D") == EDIT_TYPE.DELETED
assert CodeCommitProvider._get_edit_type("M") == EDIT_TYPE.MODIFIED 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("r") == EDIT_TYPE.RENAMED
assert CodeCommitProvider._get_edit_type("X") is None 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