diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index d5808409..aef129a5 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -138,6 +138,34 @@ class GitProvider(ABC): final_update_message=True): self.publish_comment(pr_comment) + def publish_persistent_comment_full(self, pr_comment: str, + initial_header: str, + update_header: bool = True, + name='review', + final_update_message=True): + try: + prev_comments = list(self.get_issue_comments()) + for comment in prev_comments: + if comment.body.startswith(initial_header): + latest_commit_url = self.get_latest_commit_url() + comment_url = self.get_comment_url(comment) + if update_header: + updated_header = f"{initial_header}\n\n#### ({name.capitalize()} updated until commit {latest_commit_url})\n" + pr_comment_updated = pr_comment.replace(initial_header, updated_header) + else: + pr_comment_updated = pr_comment + get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message") + # response = self.mr.notes.update(comment.id, {'body': pr_comment_updated}) + self.edit_comment(comment, pr_comment_updated) + if final_update_message: + self.publish_comment( + f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}") + return + except Exception as e: + get_logger().exception(f"Failed to update persistent review, error: {e}") + pass + self.publish_comment(pr_comment) + @abstractmethod def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): pass diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index 4baccb9e..c7fffded 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -231,24 +231,7 @@ class GithubProvider(GitProvider): update_header: bool = True, name='review', final_update_message=True): - prev_comments = list(self.pr.get_issue_comments()) - for comment in prev_comments: - body = comment.body - if body.startswith(initial_header): - latest_commit_url = self.get_latest_commit_url() - comment_url = self.get_comment_url(comment) - if update_header: - updated_header = f"{initial_header}\n\n#### ({name.capitalize()} updated until commit {latest_commit_url})\n" - pr_comment_updated = pr_comment.replace(initial_header, updated_header) - else: - pr_comment_updated = pr_comment - get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message") - response = comment.edit(pr_comment_updated) - if final_update_message: - self.publish_comment( - f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}") - return - self.publish_comment(pr_comment) + self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message) def publish_comment(self, pr_comment: str, is_temporary: bool = False): if is_temporary and not get_settings().config.publish_output_progress: diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 766a6743..0461535b 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -173,26 +173,7 @@ class GitLabProvider(GitProvider): update_header: bool = True, name='review', final_update_message=True): - try: - for comment in self.mr.notes.list(get_all=True)[::-1]: - if comment.body.startswith(initial_header): - latest_commit_url = self.get_latest_commit_url() - comment_url = self.get_comment_url(comment) - if update_header: - updated_header = f"{initial_header}\n\n#### ({name.capitalize()} updated until commit {latest_commit_url})\n" - pr_comment_updated = pr_comment.replace(initial_header, updated_header) - else: - pr_comment_updated = pr_comment - get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message") - response = self.mr.notes.update(comment.id, {'body': pr_comment_updated}) - if final_update_message: - self.publish_comment( - f"**[Persistent {name}]({comment_url})** updated to latest commit {latest_commit_url}") - return - except Exception as e: - get_logger().exception(f"Failed to update persistent review, error: {e}") - pass - self.publish_comment(pr_comment) + self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message) def publish_comment(self, mr_comment: str, is_temporary: bool = False): comment = self.mr.notes.create({'body': mr_comment}) @@ -203,6 +184,11 @@ class GitLabProvider(GitProvider): def edit_comment(self, comment, body: str): self.mr.notes.update(comment.id,{'body': body} ) + def edit_comment_from_comment_id(self, comment_id: int, body: str): + comment = self.mr.notes.get(comment_id) + comment.body = body + comment.save() + def reply_to_comment_from_comment_id(self, comment_id: int, body: str): discussion = self.mr.discussions.get(comment_id) discussion.notes.create({'body': body}) @@ -219,6 +205,10 @@ class GitLabProvider(GitProvider): def create_inline_comments(self, comments: list[dict]): raise NotImplementedError("Gitlab provider does not support publishing inline comments yet") + def get_comment_body_from_comment_id(self, comment_id: int): + comment = self.mr.notes.get(comment_id) + return comment + def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int, source_line_no: int, target_file: str,target_line_no: int) -> None: if not found: @@ -381,7 +371,7 @@ class GitLabProvider(GitProvider): return self.mr.description def get_issue_comments(self): - raise NotImplementedError("GitLab provider does not support issue comments yet") + return self.mr.notes.list(get_all=True)[::-1] def get_repo_settings(self): try: diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index fe5e325e..4fc6ae5c 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -92,7 +92,8 @@ commitable_code_suggestions = false extra_instructions = "" rank_suggestions = false enable_help_text=false -persistent_comment=false +persistent_comment=true +max_history_len=4 # enable to apply suggestion 💎 apply_suggestions_checkbox=true # suggestions scoring diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index 0e39a6cf..429811a5 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -17,6 +17,7 @@ from pr_agent.log import get_logger from pr_agent.servers.help import HelpMessage from pr_agent.tools.pr_description import insert_br_after_x_chars import difflib +import re class PRCodeSuggestions: @@ -132,11 +133,12 @@ class PRCodeSuggestions: if get_settings().pr_code_suggestions.persistent_comment: final_update_message = False - self.git_provider.publish_persistent_comment(pr_body, + self.publish_persistent_comment_with_history(pr_body, initial_header="## PR Code Suggestions ✨", update_header=True, name="suggestions", - final_update_message=final_update_message, ) + final_update_message = final_update_message, + max_previous_comments = get_settings().pr_code_suggestions.max_history_len) if self.progress_response: self.progress_response.delete() else: @@ -160,6 +162,101 @@ class PRCodeSuggestions: self.git_provider.publish_comment(f"Failed to generate code suggestions for PR") except Exception as e: pass + def publish_persistent_comment_with_history(self, pr_comment: str, + initial_header: str, + update_header: bool = True, + name='review', + final_update_message=True, + max_previous_comments=4): + history_header = f"#### Previous Suggestions\n" + last_commit_num = self.git_provider.get_latest_commit_url().split('/')[-1][:7] + latest_suggestion_header = f"Latest Suggestions up to {last_commit_num}" + latest_commit_html_comment = f"" + found_comment = None + + if max_previous_comments > 0: + try: + prev_comments = list(self.git_provider.get_issue_comments()) + for comment in prev_comments: + if comment.body.startswith(initial_header): + prev_suggestions = comment.body + found_comment = comment + comment_url = self.git_provider.get_comment_url(comment) + + if history_header.strip() not in comment.body: + # no history section + # extract everything between and
in comment.body including and
+ table_index = comment.body.find("") + if table_index == -1: + self.git_provider.edit_comment(comment, pr_comment) + continue + # find http link from comment.body[:table_index] + up_to_commit_txt = self.extract_link(comment.body[:table_index]) + prev_suggestion_table = comment.body[table_index:comment.body.rfind("
") + len("")] + + tick = "✅ " if "✅" in prev_suggestion_table else "" + # surround with details tag + prev_suggestion_table = f"
{tick}{name.capitalize()}{up_to_commit_txt}\n
{prev_suggestion_table}\n\n
" + + new_suggestion_table = pr_comment.replace(initial_header, "").strip() + + pr_comment_updated = f"{initial_header}\n{latest_commit_html_comment}\n\n" + pr_comment_updated += f"{latest_suggestion_header}\n{new_suggestion_table}\n\n___\n\n" + pr_comment_updated += f"{history_header}{prev_suggestion_table}\n" + else: + # get the text of the previous suggestions until the latest commit + sections = prev_suggestions.split(history_header.strip()) + latest_table = sections[0].strip() + prev_suggestion_table = sections[1].replace(history_header, "").strip() + + # get text after the latest_suggestion_header in comment.body + table_ind = latest_table.find("") + up_to_commit_txt = self.extract_link(latest_table[:table_ind]) + + latest_table = latest_table[table_ind:latest_table.rfind("
") + len("")] + # enforce max_previous_comments + count = prev_suggestions.count(f"\n
{name.capitalize()}") + count += prev_suggestions.count(f"\n
✅ {name.capitalize()}") + if count >= max_previous_comments: + # remove the oldest suggestion + prev_suggestion_table = prev_suggestion_table[:prev_suggestion_table.rfind(f"
{name.capitalize()} up to commit")] + + tick = "✅ " if "✅" in latest_table else "" + # Add to the prev_suggestions section + last_prev_table = f"\n
{tick}{name.capitalize()}{up_to_commit_txt}\n
{latest_table}\n\n
" + prev_suggestion_table = last_prev_table + "\n" + prev_suggestion_table + new_suggestion_table = pr_comment.replace(initial_header, "").strip() + + pr_comment_updated = f"{initial_header}\n" + pr_comment_updated += f"{latest_commit_html_comment}\n\n" + pr_comment_updated += f"{latest_suggestion_header}\n\n{new_suggestion_table}\n\n" + pr_comment_updated += "___\n\n" + pr_comment_updated += f"{history_header}\n" + pr_comment_updated += f"{prev_suggestion_table}\n" + + get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message") + + self.git_provider.edit_comment(comment, pr_comment_updated) + return + except Exception as e: + get_logger().exception(f"Failed to update persistent review, error: {e}") + pass + + body = pr_comment.replace(initial_header, "").strip() + pr_comment = f"{initial_header}\n\n{latest_commit_html_comment}\n\n{body}\n\n" + if found_comment is not None: + self.git_provider.edit_comment(found_comment, pr_comment) + else: + self.git_provider.publish_comment(pr_comment) + + def extract_link(self, s): + r = re.compile(r"") + match = r.search(s) + + up_to_commit_txt = "" + if match: + up_to_commit_txt = f" up to commit {match.group(0)[4:-3].strip()}" + return up_to_commit_txt async def _prepare_prediction(self, model: str) -> dict: self.patches_diff = get_pr_diff(self.git_provider,