diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 67735ee0..4b0fba42 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -53,8 +53,7 @@ class GitProvider(ABC): pass @abstractmethod - def publish_code_suggestion(self, body: str, relevant_file: str, - relevant_lines_start: int, relevant_lines_end: int): + def publish_code_suggestions(self, code_suggestions: list): pass @abstractmethod diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index 7c6257a1..9b6d6d8f 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -146,24 +146,32 @@ class GithubProvider(GitProvider): def publish_inline_comments(self, comments: list[dict]): self.pr.create_review(commit=self.last_commit_id, comments=comments) - def publish_code_suggestion(self, body: str, - relevant_file: str, - relevant_lines_start: int, - relevant_lines_end: int): - if not relevant_lines_start or relevant_lines_start == -1: - if settings.config.verbosity_level >= 2: - logging.exception(f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}") - return False + def publish_code_suggestions(self, code_suggestions: list): + """ + Publishes code suggestions as comments on the PR. + In practice current APU enables to send only one code suggestion per comment. Might change in the future. + """ + post_parameters_list = [] + import github.PullRequestComment + for suggestion in code_suggestions: + body = suggestion['body'] + relevant_file = suggestion['relevant_file'] + relevant_lines_start = suggestion['relevant_lines_start'] + relevant_lines_end = suggestion['relevant_lines_end'] - if relevant_lines_end < relevant_lines_start: - if settings.config.verbosity_level >= 2: - logging.exception(f"Failed to publish code suggestion, " - f"relevant_lines_end is {relevant_lines_end} and " - f"relevant_lines_start is {relevant_lines_start}") - return False + if not relevant_lines_start or relevant_lines_start == -1: + if settings.config.verbosity_level >= 2: + logging.exception( + f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}") + continue + + if relevant_lines_end < relevant_lines_start: + if settings.config.verbosity_level >= 2: + logging.exception(f"Failed to publish code suggestion, " + f"relevant_lines_end is {relevant_lines_end} and " + f"relevant_lines_start is {relevant_lines_start}") + continue - try: - import github.PullRequestComment if relevant_lines_end > relevant_lines_start: post_parameters = { "body": body, @@ -181,17 +189,19 @@ class GithubProvider(GitProvider): "line": relevant_lines_start, "side": "RIGHT", } - headers, data = self.pr._requester.requestJsonAndCheck( - "POST", f"{self.pr.url}/comments", input=post_parameters - ) - github.PullRequestComment.PullRequestComment( - self.pr._requester, headers, data, completed=True - ) - return True - except Exception as e: - if settings.config.verbosity_level >= 2: - logging.error(f"Failed to publish code suggestion, error: {e}") - return False + + try: + headers, data = self.pr._requester.requestJsonAndCheck( + "POST", f"{self.pr.url}/comments", input=post_parameters + ) + github.PullRequestComment.PullRequestComment( + self.pr._requester, headers, data, completed=True + ) + return True + except Exception as e: + if settings.config.verbosity_level >= 2: + logging.error(f"Failed to publish code suggestion, error: {e}") + return False def remove_initial_comment(self): try: @@ -314,7 +324,6 @@ class GithubProvider(GitProvider): for p in pr_types: color = label_color_map.get(p, "d1bcf9") # default to "Other" color post_parameters.append({"name": p, "color": color}) - post_parameters.append({"name": p, "color": colors[ind]}) headers, data = self.pr._requester.requestJsonAndCheck( "PUT", f"{self.pr.issue_url}/labels", input=post_parameters ) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 089d3fca..d9efe1d8 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -135,26 +135,29 @@ class GitLabProvider(GitProvider): self.mr.discussions.create({'body': body, 'position': pos_obj}) - def publish_code_suggestion(self, body: str, - relevant_file: str, - relevant_lines_start: int, - relevant_lines_end: int): - self.diff_files = self.diff_files if self.diff_files else self.get_diff_files() - target_file = None - for file in self.diff_files: - if file.filename == relevant_file: - if file.filename == relevant_file: - target_file = file - break - range = relevant_lines_end - relevant_lines_start + 1 - body = body.replace('```suggestion', f'```suggestion:-0+{range}') + def publish_code_suggestions(self, code_suggestions: list): + for suggestion in code_suggestions: + body = suggestion['body'] + relevant_file = suggestion['relevant_file'] + relevant_lines_start = suggestion['relevant_lines_start'] + relevant_lines_end = suggestion['relevant_lines_end'] - lines = target_file.head_file.splitlines() - relevant_line_in_file = lines[relevant_lines_start - 1] - edit_type, found, source_line_no, target_file, target_line_no = self.find_in_file(target_file, - relevant_line_in_file) - self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, - target_file, target_line_no) + self.diff_files = self.diff_files if self.diff_files else self.get_diff_files() + target_file = None + for file in self.diff_files: + if file.filename == relevant_file: + if file.filename == relevant_file: + target_file = file + break + range = relevant_lines_end - relevant_lines_start + 1 + body = body.replace('```suggestion', f'```suggestion:-0+{range}') + + lines = target_file.head_file.splitlines() + relevant_line_in_file = lines[relevant_lines_start - 1] + edit_type, found, source_line_no, target_file, target_line_no = self.find_in_file(target_file, + relevant_line_in_file) + self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, + target_file, target_line_no) def search_line(self, relevant_file, relevant_line_in_file): target_file = None diff --git a/pr_agent/servers/help.py b/pr_agent/servers/help.py index cfb6bd35..f6b5af9a 100644 --- a/pr_agent/servers/help.py +++ b/pr_agent/servers/help.py @@ -1,9 +1,9 @@ -commands_text = "> /review [-i]: Request a review of your Pull Request. For an incremental review, which only " \ +commands_text = "> **/review [-i]**: Request a review of your Pull Request. For an incremental review, which only " \ "considers changes since the last review, include the '-i' option.\n" \ - "> /describe: Modify the PR title and description based on the contents of the PR.\n" \ - "> /improve: Suggest improvements to the code in the PR. " \ + "> **/describe**: Modify the PR title and description based on the contents of the PR.\n" \ + "> **/improve**: Suggest improvements to the code in the PR. " \ "These will be provided as pull request comments, ready to commit.\n" \ - "> /ask \\: Pose a question about the PR.\n" + "> **/ask \\**: Pose a question about the PR.\n" def bot_help_text(user: str): diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index d8203443..bfc06b5c 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -79,7 +79,6 @@ class PRCodeSuggestions: def _prepare_pr_code_suggestions(self) -> str: review = self.prediction.strip() - data = None try: data = json.loads(review) except json.decoder.JSONDecodeError: @@ -89,6 +88,7 @@ class PRCodeSuggestions: return data def push_inline_code_suggestions(self, data): + code_suggestions = [] for d in data['Code suggestions']: if settings.config.verbosity_level >= 2: logging.info(f"suggestion: {d}") @@ -100,27 +100,33 @@ class PRCodeSuggestions: new_code_snippet = d['improved code'] if new_code_snippet: - try: # dedent code snippet - self.diff_files = self.git_provider.diff_files if self.git_provider.diff_files \ - else self.git_provider.get_diff_files() - original_initial_line = None - for file in self.diff_files: - if file.filename.strip() == relevant_file: - original_initial_line = file.head_file.splitlines()[relevant_lines_start - 1] - break - if original_initial_line: - suggested_initial_line = new_code_snippet.splitlines()[0] - original_initial_spaces = len(original_initial_line) - len(original_initial_line.lstrip()) - suggested_initial_spaces = len(suggested_initial_line) - len(suggested_initial_line.lstrip()) - delta_spaces = original_initial_spaces - suggested_initial_spaces - if delta_spaces > 0: - new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n') - except Exception as e: - if settings.config.verbosity_level >= 2: - logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}") + new_code_snippet = self.dedent_code(relevant_file, relevant_lines_start, new_code_snippet) body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```" - self.git_provider.publish_code_suggestion(body=body, - relevant_file=relevant_file, - relevant_lines_start=relevant_lines_start, - relevant_lines_end=relevant_lines_end) + code_suggestions.append({'body': body,'relevant_file': relevant_file, + 'relevant_lines_start': relevant_lines_start, + 'relevant_lines_end': relevant_lines_end}) + + self.git_provider.publish_code_suggestions(code_suggestions) + + def dedent_code(self, relevant_file, relevant_lines_start, new_code_snippet): + try: # dedent code snippet + self.diff_files = self.git_provider.diff_files if self.git_provider.diff_files \ + else self.git_provider.get_diff_files() + original_initial_line = None + for file in self.diff_files: + if file.filename.strip() == relevant_file: + original_initial_line = file.head_file.splitlines()[relevant_lines_start - 1] + break + if original_initial_line: + suggested_initial_line = new_code_snippet.splitlines()[0] + original_initial_spaces = len(original_initial_line) - len(original_initial_line.lstrip()) + suggested_initial_spaces = len(suggested_initial_line) - len(suggested_initial_line.lstrip()) + delta_spaces = original_initial_spaces - suggested_initial_spaces + if delta_spaces > 0: + new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n') + except Exception as e: + if settings.config.verbosity_level >= 2: + logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}") + + return new_code_snippet