diff --git a/INSTALL.md b/INSTALL.md
index 88ad92bb..ba2547b1 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -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": "*"
}
diff --git a/README.md b/README.md
index 47dca106..faab0af1 100644
--- a/README.md
+++ b/README.md
@@ -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: | | | |
diff --git a/pr_agent/git_providers/codecommit_client.py b/pr_agent/git_providers/codecommit_client.py
index c1cfa763..6200340d 100644
--- a/pr_agent/git_providers/codecommit_client.py
+++ b/pr_agent/git_providers/codecommit_client.py
@@ -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()
diff --git a/pr_agent/git_providers/codecommit_provider.py b/pr_agent/git_providers/codecommit_provider.py
index a747e7f2..d43409c3 100644
--- a/pr_agent/git_providers/codecommit_provider.py
+++ b/pr_agent/git_providers/codecommit_provider.py
@@ -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'(? 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("", "")
+ comment = comment.replace(" ", "")
+ comment = comment.replace("", "")
+ comment = comment.replace("", "")
+ 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:
diff --git a/tests/unittest/test_codecommit_provider.py b/tests/unittest/test_codecommit_provider.py
index e35f7250..9de7c45c 100644
--- a/tests/unittest/test_codecommit_provider.py
+++ b/tests/unittest/test_codecommit_provider.py
@@ -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\nCode feedback:
\nfile foo\n\n"
+ expect = "## PR Feedback\nCode feedback:\nfile foo\n\n"
+ assert CodeCommitProvider._remove_markdown_html(input) == expect
\ No newline at end of file