diff --git a/pr_agent/git_providers/bitbucket_provider.py b/pr_agent/git_providers/bitbucket_provider.py index bb7b2c1d..62ec0a19 100644 --- a/pr_agent/git_providers/bitbucket_provider.py +++ b/pr_agent/git_providers/bitbucket_provider.py @@ -26,7 +26,7 @@ class BitbucketProvider: self.set_pr(pr_url) def is_supported(self, capability: str) -> bool: - if capability == 'get_issue_comments': + if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']: return False return True @@ -64,6 +64,12 @@ class BitbucketProvider: def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): pass + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): + raise NotImplementedError("Bitbucket provider does not support creating inline comments yet") + + def publish_inline_comments(self, comments: list[dict]): + raise NotImplementedError("Bitbucket provider does not support publishing inline comments yet") + def get_title(self): return self.pr.title diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 55d2bf34..f7c7fa98 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -44,6 +44,14 @@ class GitProvider(ABC): def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): pass + @abstractmethod + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): + pass + + @abstractmethod + def publish_inline_comments(self, comments: list[dict]): + pass + @abstractmethod def publish_code_suggestion(self, body: str, relevant_file: str, relevant_lines_start: int, relevant_lines_end: int): diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index 76f3a818..a04f550a 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Optional, Tuple from urllib.parse import urlparse -from github import AppAuthentication, Github +from github import AppAuthentication, Github, Auth from pr_agent.config_loader import settings @@ -57,6 +57,9 @@ class GithubProvider(GitProvider): self.pr.comments_list.append(response) def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): + self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)]) + + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): self.diff_files = self.diff_files if self.diff_files else self.get_diff_files() position = -1 for file in self.diff_files: @@ -75,9 +78,16 @@ class GithubProvider(GitProvider): if position == -1: if settings.config.verbosity_level >= 2: logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") + subject_type = "FILE" else: - path = relevant_file.strip() - self.pr.create_review_comment(body=body, commit_id=self.last_commit_id, path=path, position=position) + subject_type = "LINE" + path = relevant_file.strip() + # placeholder for future API support (already supported in single inline comment) + # return dict(body=body, path=path, position=position, subject_type=subject_type) + return dict(body=body, path=path, position=position) if subject_type == "LINE" else {} + + 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, @@ -218,7 +228,7 @@ class GithubProvider(GitProvider): raise ValueError( "GitHub token is required when using user deployment. See: " "https://github.com/Codium-ai/pr-agent#method-2-run-from-source") from e - return Github(token) + return Github(auth=Auth.Token(token)) def _get_repo(self): return self.github_client.get_repo(self.repo) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index d83ae6d5..c3161dfb 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -33,7 +33,7 @@ class GitLabProvider(GitProvider): r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") def is_supported(self, capability: str) -> bool: - if capability == 'get_issue_comments': + if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']: return False return True @@ -102,6 +102,12 @@ class GitLabProvider(GitProvider): self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, target_file, target_line_no) + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): + raise NotImplementedError("Gitlab provider does not support creating inline comments yet") + + def create_inline_comment(self, comments: list[dict]): + raise NotImplementedError("Gitlab provider does not support publishing inline comments yet") + def send_inline_comment(self, body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, target_file, target_line_no): if not found: diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index ab6ffe9b..542e50b6 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -51,7 +51,7 @@ class PRReviewer: async def review(self): logging.info('Reviewing PR...') if settings.config.publish_output: - self.git_provider.publish_comment("Preparing review...", is_temporary=True) + self.git_provider.publish_comment("Preparing review...", is_temporary=True) logging.info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) logging.info('Getting AI prediction...') @@ -99,7 +99,13 @@ class PRReviewer: if settings.config.git_provider == 'github' and \ settings.pr_reviewer.inline_code_comments and \ 'Code suggestions' in data['PR Feedback']: - del data['PR Feedback']['Code suggestions'] + # keeping only code suggestions that can't be submitted as inline comments + data['PR Feedback']['Code suggestions'] = [ + d for d in data['PR Feedback']['Code suggestions'] + if any(key not in d for key in ('relevant file', 'relevant line in file', 'suggestion content')) + ] + if not data['PR Feedback']['Code suggestions']: + del data['PR Feedback']['Code suggestions'] markdown_text = convert_to_markdown(data) user = self.git_provider.get_user_id() @@ -125,16 +131,24 @@ class PRReviewer: except json.decoder.JSONDecodeError: data = try_fix_json(review) - if settings.pr_reviewer.num_code_suggestions > 0: - try: - for d in data['PR Feedback']['Code suggestions']: - relevant_file = d['relevant file'].strip() - relevant_line_in_file = d['relevant line in file'].strip() - content = d['suggestion content'] + comments = [] + for d in data['PR Feedback']['Code suggestions']: + relevant_file = d.get('relevant file', '').strip() + relevant_line_in_file = d.get('relevant line in file', '').strip() + content = d.get('suggestion content', '') + if not relevant_file or not relevant_line_in_file or not content: + logging.info("Skipping inline comment with missing file/line/content") + continue - self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) - except KeyError: - pass + if self.git_provider.is_supported("create_inline_comment"): + comment = self.git_provider.create_inline_comment(content, relevant_file, relevant_line_in_file) + if comment: + comments.append(comment) + else: + self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) + + if comments: + self.git_provider.publish_inline_comments(comments) def _get_user_answers(self): answer_str = question_str = "" diff --git a/requirements.txt b/requirements.txt index 8695f709..399cd3ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ dynaconf==3.1.12 fastapi==0.99.0 -PyGithub==1.58.2 +PyGithub==1.59.* retry==0.9.2 openai==0.27.8 Jinja2==3.1.2