diff --git a/README.md b/README.md index f946a59c..5bf7d52e 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ See the [usage guide](./Usage.md) for instructions how to run the different tool | TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Auto-Description | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: | -| | Improve Code | :white_check_mark: | :white_check_mark: | | | | -| | ⮑ Extended | :white_check_mark: | :white_check_mark: | | | | +| | Improve Code | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | +| | ⮑ Extended | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | | | Reflect and Review | :white_check_mark: | | | | :white_check_mark: | | | Update CHANGELOG.md | :white_check_mark: | | | | | | | | | | | | | diff --git a/pr_agent/git_providers/codecommit_client.py b/pr_agent/git_providers/codecommit_client.py index 6200340d..1112ee22 100644 --- a/pr_agent/git_providers/codecommit_client.py +++ b/pr_agent/git_providers/codecommit_client.py @@ -90,7 +90,11 @@ class CodeCommitClient: ): differences.extend(page.get("differences", [])) except botocore.exceptions.ClientError as e: - raise ValueError(f"Failed to retrieve differences from CodeCommit PR #{self.pr_num}") from e + if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException': + raise ValueError(f"CodeCommit cannot retrieve differences: Repository does not exist: {repo_name}") from e + raise ValueError(f"CodeCommit cannot retrieve differences for {source_commit}..{destination_commit}") from e + except Exception as e: + raise ValueError(f"CodeCommit cannot retrieve differences for {source_commit}..{destination_commit}") from e output = [] for json in differences: @@ -122,6 +126,8 @@ class CodeCommitClient: try: response = self.boto_client.get_file(repositoryName=repo_name, commitSpecifier=sha_hash, filePath=file_path) except botocore.exceptions.ClientError as e: + if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException': + raise ValueError(f"CodeCommit cannot retrieve PR: Repository does not exist: {repo_name}") from e # if the file does not exist, but is flagged as optional, then return an empty string if optional and e.response["Error"]["Code"] == 'FileDoesNotExistException': return "" @@ -133,11 +139,12 @@ class CodeCommitClient: return response.get("fileContent", "") - def get_pr(self, pr_number: int): + def get_pr(self, repo_name: str, pr_number: int): """ Get a information about a CodeCommit PR. Args: + - repo_name: Name of the repository - pr_number: The PR number you are requesting Returns: @@ -155,6 +162,8 @@ class CodeCommitClient: except botocore.exceptions.ClientError as e: if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException': raise ValueError(f"CodeCommit cannot retrieve PR: PR number does not exist: {pr_number}") from e + if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException': + raise ValueError(f"CodeCommit cannot retrieve PR: Repository does not exist: {repo_name}") from e raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}: boto client error") from e except Exception as e: raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}") from e @@ -201,7 +210,7 @@ class CodeCommitClient: 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, annotation_file: str = None, annotation_line: int = None): """ Publish a comment to a pull request @@ -210,7 +219,13 @@ class CodeCommitClient: - 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) - source_commit: The commit hash of the code you are adding (the "after" branch) - - pr_comment: comment + - comment: The comment you want to publish + - annotation_file: The file you want to annotate (optional) + - annotation_line: The line number you want to annotate (optional) + + Comment annotations for CodeCommit are different than GitHub. + CodeCommit only designates the starting line number for the comment. + It does not support the ending line number to highlight a range of lines. Returns: - None @@ -223,13 +238,30 @@ class CodeCommitClient: self._connect_boto_client() try: - self.boto_client.post_comment_for_pull_request( - pullRequestId=str(pr_number), - repositoryName=repo_name, - beforeCommitId=destination_commit, - afterCommitId=source_commit, - content=comment, - ) + # If the comment has code annotations, + # then set the file path and line number in the location dictionary + if annotation_file and annotation_line: + self.boto_client.post_comment_for_pull_request( + pullRequestId=str(pr_number), + repositoryName=repo_name, + beforeCommitId=destination_commit, + afterCommitId=source_commit, + content=comment, + location={ + "filePath": annotation_file, + "filePosition": annotation_line, + "relativeFileVersion": "AFTER", + }, + ) + else: + # The comment does not have code annotations + self.boto_client.post_comment_for_pull_request( + pullRequestId=str(pr_number), + repositoryName=repo_name, + beforeCommitId=destination_commit, + afterCommitId=source_commit, + content=comment, + ) except botocore.exceptions.ClientError as e: if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException': raise ValueError(f"Repository does not exist: {repo_name}") from e diff --git a/pr_agent/git_providers/codecommit_provider.py b/pr_agent/git_providers/codecommit_provider.py index d43409c3..1f570a1a 100644 --- a/pr_agent/git_providers/codecommit_provider.py +++ b/pr_agent/git_providers/codecommit_provider.py @@ -180,10 +180,37 @@ class CodeCommitProvider(GitProvider): comment=pr_comment, ) except Exception as e: - raise ValueError(f"CodeCommit Cannot post comment for PR: {self.pr_num}") from e + raise ValueError(f"CodeCommit Cannot publish comment for PR: {self.pr_num}") from e def publish_code_suggestions(self, code_suggestions: list) -> bool: - return [""] # not implemented yet + counter = 1 + for suggestion in code_suggestions: + # Verify that each suggestion has the required keys + if not all(key in suggestion for key in ["body", "relevant_file", "relevant_lines_start"]): + logging.warning(f"Skipping code suggestion #{counter}: Each suggestion must have 'body', 'relevant_file', 'relevant_lines_start' keys") + continue + + # Publish the code suggestion to CodeCommit + try: + logging.debug(f"Code Suggestion #{counter} in file: {suggestion['relevant_file']}: {suggestion['relevant_lines_start']}") + self.codecommit_client.publish_comment( + repo_name=self.repo_name, + pr_number=self.pr_num, + destination_commit=self.pr.destination_commit, + source_commit=self.pr.source_commit, + comment=suggestion["body"], + annotation_file=suggestion["relevant_file"], + annotation_line=suggestion["relevant_lines_start"], + ) + except Exception as e: + raise ValueError(f"CodeCommit Cannot publish code suggestions for PR: {self.pr_num}") from e + + counter += 1 + + # The calling function passes in a list of code suggestions, and this function publishes each suggestion one at a time. + # If we were to return False here, the calling function will attempt to publish the same list of code suggestions again, one at a time. + # Since this function publishes the suggestions one at a time anyway, we always return True here to avoid the retry. + return True def publish_labels(self, labels): return [""] # not implemented yet @@ -195,6 +222,7 @@ class CodeCommitProvider(GitProvider): return "" # not implemented yet def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): + # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet") def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): @@ -255,9 +283,11 @@ class CodeCommitProvider(GitProvider): return self.codecommit_client.get_file(self.repo_name, settings_filename, self.pr.source_commit, optional=True) def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: + logging.info("CodeCommit provider does not support eyes reaction yet") return True def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: + logging.info("CodeCommit provider does not support removing reactions yet") return True @staticmethod @@ -315,16 +345,16 @@ class CodeCommitProvider(GitProvider): 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) + response = self.codecommit_client.get_pr(self.repo_name, self.pr_num) if len(response.targets) == 0: raise ValueError(f"No files found in CodeCommit PR: {self.pr_num}") - # TODO: implement support for multiple commits in one CodeCommit PR - # for now, we are only using the first commit in the PR + # TODO: implement support for multiple targets in one CodeCommit PR + # for now, we are only using the first target in the PR if len(response.targets) > 1: logging.warning( - "Multiple commits in one PR is not supported for CodeCommit yet. Continuing, using the first commit only..." + "Multiple targets in one PR is not supported for CodeCommit yet. Continuing, using the first target only..." ) # Return our object that mimics PullRequest class from the PyGithub library diff --git a/tests/unittest/test_codecommit_client.py b/tests/unittest/test_codecommit_client.py index 5d09bdd1..0aa1ffa6 100644 --- a/tests/unittest/test_codecommit_client.py +++ b/tests/unittest/test_codecommit_client.py @@ -125,7 +125,7 @@ class TestCodeCommitProvider: } } - pr = api.get_pr(321) + pr = api.get_pr("my_test_repo", 321) assert pr.title == "My PR" assert pr.description == "My PR description"