mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-19 03:50:40 +08:00
Compare commits
23 Commits
ok/remove_
...
ok/lint
Author | SHA1 | Date | |
---|---|---|---|
90f97b0226 | |||
9e0f5f0ccc | |||
87ea0176b9 | |||
62f08f4ec4 | |||
fe0058f25f | |||
6d2673f39d | |||
b3a1d456b2 | |||
f77a5f6929 | |||
fdeae9c209 | |||
a994ec1427 | |||
e5259e2f5c | |||
6f1b418b25 | |||
51e08c3c2b | |||
4c29ff2db1 | |||
5fbaa4366f | |||
aee08ebbfe | |||
6ad8df6be7 | |||
539edcad3c | |||
ea27c63f13 | |||
c866288b0a | |||
8ae3c60670 | |||
f8f415eb75 | |||
fa90b242e3 |
@ -78,6 +78,7 @@ To set up your own PR-Agent, see the [Quickstart](#Quickstart) section
|
|||||||
| | Ask | :white_check_mark: | :white_check_mark: | |
|
| | Ask | :white_check_mark: | :white_check_mark: | |
|
||||||
| | Auto-Description | :white_check_mark: | | |
|
| | Auto-Description | :white_check_mark: | | |
|
||||||
| | Improve Code | :white_check_mark: | :white_check_mark: | |
|
| | Improve Code | :white_check_mark: | :white_check_mark: | |
|
||||||
|
| | Reflect and Review | :white_check_mark: | | |
|
||||||
| | | | | |
|
| | | | | |
|
||||||
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| | Tagging bot | :white_check_mark: | :white_check_mark: | |
|
| | Tagging bot | :white_check_mark: | :white_check_mark: | |
|
||||||
@ -92,6 +93,7 @@ Examples for invoking the different tools via the [CLI](#quickstart):
|
|||||||
- **Describe**: python cli.py --pr-url=<pr_url> describe
|
- **Describe**: python cli.py --pr-url=<pr_url> describe
|
||||||
- **Improve**: python cli.py --pr-url=<pr_url> improve
|
- **Improve**: python cli.py --pr-url=<pr_url> improve
|
||||||
- **Ask**: python cli.py --pr-url=<pr_url> ask "Write me a poem about this PR"
|
- **Ask**: python cli.py --pr-url=<pr_url> ask "Write me a poem about this PR"
|
||||||
|
- **Reflect**: python cli.py --pr-url=<pr_url> reflect
|
||||||
|
|
||||||
"<pr_url>" is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).
|
"<pr_url>" is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).
|
||||||
|
|
||||||
@ -120,12 +122,13 @@ Here are several ways to install and run PR-Agent:
|
|||||||
|
|
||||||
## Usage and Tools
|
## Usage and Tools
|
||||||
|
|
||||||
**PR-Agent** provides four types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"` and `"PR Code Sueggestions"`.
|
**PR-Agent** provides five types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"`, `"PR Code Sueggestions"` and `"PR Reflect and Review"`.
|
||||||
|
|
||||||
- The "PR Reviewer" tool automatically analyzes PRs, and provides various types of feedback.
|
- The "PR Reviewer" tool automatically analyzes PRs, and provides various types of feedback.
|
||||||
- The "PR Q&A" tool answers free-text questions about the PR.
|
- The "PR Q&A" tool answers free-text questions about the PR.
|
||||||
- The "PR Description" tool automatically sets the PR Title and body.
|
- The "PR Description" tool automatically sets the PR Title and body.
|
||||||
- The "PR Code Suggestion" tool provide inline code suggestions for the PR that can be applied and committed.
|
- The "PR Code Suggestion" tool provide inline code suggestions for the PR that can be applied and committed.
|
||||||
|
- The "PR Reflect and Review" tool first initiates a dialog with the user and asks them to reflect on the PR, and then provides a review.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@ -138,11 +141,11 @@ Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more detai
|
|||||||
- [ ] Support open-source models, as a replacement for openai models. (Note - a minimal requirement for each open-source model is to have 8k+ context, and good support for generating json as an output)
|
- [ ] Support open-source models, as a replacement for openai models. (Note - a minimal requirement for each open-source model is to have 8k+ context, and good support for generating json as an output)
|
||||||
- [x] Support other Git providers, such as Gitlab and Bitbucket.
|
- [x] Support other Git providers, such as Gitlab and Bitbucket.
|
||||||
- [ ] Develop additional logics for handling large PRs, and compressing git patches
|
- [ ] Develop additional logics for handling large PRs, and compressing git patches
|
||||||
- [ ] Dedicated tools and sub-tools for specific programming languages (Python, Javascript, Java, C++, etc)
|
|
||||||
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
|
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
|
||||||
- [ ] Adding more tools. Possible directions:
|
- [ ] Adding more tools. Possible directions:
|
||||||
- [x] PR description
|
- [x] PR description
|
||||||
- [x] Inline code suggestions
|
- [x] Inline code suggestions
|
||||||
|
- [x] Reflect and review
|
||||||
- [ ] Enforcing CONTRIBUTING.md guidelines
|
- [ ] Enforcing CONTRIBUTING.md guidelines
|
||||||
- [ ] Performance (are there any performance issues)
|
- [ ] Performance (are there any performance issues)
|
||||||
- [ ] Documentation (is the PR properly documented)
|
- [ ] Documentation (is the PR properly documented)
|
||||||
|
BIN
pics/.DS_Store
vendored
BIN
pics/.DS_Store
vendored
Binary file not shown.
@ -1,7 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
from pr_agent.config_loader import settings
|
||||||
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
||||||
from pr_agent.tools.pr_description import PRDescription
|
from pr_agent.tools.pr_description import PRDescription
|
||||||
|
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
|
||||||
from pr_agent.tools.pr_questions import PRQuestions
|
from pr_agent.tools.pr_questions import PRQuestions
|
||||||
from pr_agent.tools.pr_reviewer import PRReviewer
|
from pr_agent.tools.pr_reviewer import PRReviewer
|
||||||
|
|
||||||
@ -11,8 +13,13 @@ class PRAgent:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def handle_request(self, pr_url, request) -> bool:
|
async def handle_request(self, pr_url, request) -> bool:
|
||||||
if any(cmd in request for cmd in ["/review", "/review_pr"]):
|
if any(cmd in request for cmd in ["/answer"]):
|
||||||
await PRReviewer(pr_url).review()
|
await PRReviewer(pr_url, is_answer=True).review()
|
||||||
|
elif any(cmd in request for cmd in ["/review", "/review_pr", "/reflect_and_review"]):
|
||||||
|
if settings.pr_reviewer.ask_and_reflect or "/reflect_and_review" in request:
|
||||||
|
await PRInformationFromUser(pr_url).generate_questions()
|
||||||
|
else:
|
||||||
|
await PRReviewer(pr_url).review()
|
||||||
elif any(cmd in request for cmd in ["/describe", "/describe_pr"]):
|
elif any(cmd in request for cmd in ["/describe", "/describe_pr"]):
|
||||||
await PRDescription(pr_url).describe()
|
await PRDescription(pr_url).describe()
|
||||||
elif any(cmd in request for cmd in ["/improve", "/improve_code"]):
|
elif any(cmd in request for cmd in ["/improve", "/improve_code"]):
|
||||||
|
@ -158,7 +158,7 @@ def convert_to_hunks_with_lines_numbers(patch: str, file) -> str:
|
|||||||
patch_with_lines_str += f"{start2 + i} {line_new}\n"
|
patch_with_lines_str += f"{start2 + i} {line_new}\n"
|
||||||
if old_content_lines:
|
if old_content_lines:
|
||||||
patch_with_lines_str += '--old hunk--\n'
|
patch_with_lines_str += '--old hunk--\n'
|
||||||
for i, line_old in enumerate(old_content_lines):
|
for line_old in old_content_lines:
|
||||||
patch_with_lines_str += f"{line_old}\n"
|
patch_with_lines_str += f"{line_old}\n"
|
||||||
new_content_lines = []
|
new_content_lines = []
|
||||||
old_content_lines = []
|
old_content_lines = []
|
||||||
@ -179,7 +179,7 @@ def convert_to_hunks_with_lines_numbers(patch: str, file) -> str:
|
|||||||
patch_with_lines_str += f"{start2 + i} {line_new}\n"
|
patch_with_lines_str += f"{start2 + i} {line_new}\n"
|
||||||
if old_content_lines:
|
if old_content_lines:
|
||||||
patch_with_lines_str += '\n--old hunk--\n'
|
patch_with_lines_str += '\n--old hunk--\n'
|
||||||
for i, line_old in enumerate(old_content_lines):
|
for line_old in old_content_lines:
|
||||||
patch_with_lines_str += f"{line_old}\n"
|
patch_with_lines_str += f"{line_old}\n"
|
||||||
|
|
||||||
return patch_with_lines_str.strip()
|
return patch_with_lines_str.strip()
|
||||||
|
@ -4,8 +4,7 @@ import difflib
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Tuple, Union
|
from typing import Any, Tuple, Union
|
||||||
|
|
||||||
from pr_agent.algo.git_patch_processing import extend_patch, handle_patch_deletions, \
|
from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions
|
||||||
convert_to_hunks_with_lines_numbers
|
|
||||||
from pr_agent.algo.language_handler import sort_files_by_main_languages
|
from pr_agent.algo.language_handler import sort_files_by_main_languages
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
|
@ -89,8 +89,8 @@ def try_fix_json(review, max_iter=10, code_suggestions=False):
|
|||||||
data = {}
|
data = {}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def fix_json_escape_char(json_message=None):
|
def fix_json_escape_char(json_message=None):
|
||||||
result = None
|
|
||||||
try:
|
try:
|
||||||
result = json.loads(json_message)
|
result = json.loads(json_message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -100,5 +100,5 @@ def fix_json_escape_char(json_message=None):
|
|||||||
json_message = list(json_message)
|
json_message = list(json_message)
|
||||||
json_message[idx_to_replace] = ' '
|
json_message[idx_to_replace] = ' '
|
||||||
new_message = ''.join(json_message)
|
new_message = ''.join(json_message)
|
||||||
return fix_JSON(json_message=new_message)
|
return fix_json_escape_char(json_message=new_message)
|
||||||
return result
|
return result
|
||||||
|
@ -18,19 +18,22 @@ For example:
|
|||||||
- cli.py --pr-url=... describe
|
- cli.py --pr-url=... describe
|
||||||
- cli.py --pr-url=... improve
|
- cli.py --pr-url=... improve
|
||||||
- cli.py --pr-url=... ask "write me a poem about this PR"
|
- cli.py --pr-url=... ask "write me a poem about this PR"
|
||||||
|
- cli.py --pr-url=... reflect
|
||||||
|
|
||||||
Supported commands:
|
Supported commands:
|
||||||
review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement.
|
review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement.
|
||||||
ask / ask_question [question] - Ask a question about the PR.
|
ask / ask_question [question] - Ask a question about the PR.
|
||||||
describe / describe_pr - Modify the PR title and description based on the PR's contents.
|
describe / describe_pr - Modify the PR title and description based on the PR's contents.
|
||||||
improve / improve_code - Suggest improvements to the code in the PR as pull request comments ready to commit.
|
improve / improve_code - Suggest improvements to the code in the PR as pull request comments ready to commit.
|
||||||
|
reflect - Ask the PR author questions about the PR.
|
||||||
""")
|
""")
|
||||||
parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True)
|
parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True)
|
||||||
parser.add_argument('command', type=str, help='The', choices=['review', 'review_pr',
|
parser.add_argument('command', type=str, help='The', choices=['review', 'review_pr',
|
||||||
'ask', 'ask_question',
|
'ask', 'ask_question',
|
||||||
'describe', 'describe_pr',
|
'describe', 'describe_pr',
|
||||||
'improve', 'improve_code',
|
'improve', 'improve_code',
|
||||||
'user_questions'], default='review')
|
'reflect', 'review_after_reflect'],
|
||||||
|
default='review')
|
||||||
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
|
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
||||||
@ -56,10 +59,14 @@ improve / improve_code - Suggest improvements to the code in the PR as pull requ
|
|||||||
print(f"Reviewing PR: {args.pr_url}")
|
print(f"Reviewing PR: {args.pr_url}")
|
||||||
reviewer = PRReviewer(args.pr_url, cli_mode=True)
|
reviewer = PRReviewer(args.pr_url, cli_mode=True)
|
||||||
asyncio.run(reviewer.review())
|
asyncio.run(reviewer.review())
|
||||||
elif command in ['user_questions']:
|
elif command in ['reflect']:
|
||||||
print(f"Asking the PR author questions: {args.pr_url}")
|
print(f"Asking the PR author questions: {args.pr_url}")
|
||||||
reviewer = PRInformationFromUser(args.pr_url)
|
reviewer = PRInformationFromUser(args.pr_url)
|
||||||
asyncio.run(reviewer.generate_questions())
|
asyncio.run(reviewer.generate_questions())
|
||||||
|
elif command in ['review_after_reflect']:
|
||||||
|
print(f"Processing author's answers and sending review: {args.pr_url}")
|
||||||
|
reviewer = PRReviewer(args.pr_url, cli_mode=True, is_answer=True)
|
||||||
|
asyncio.run(reviewer.review())
|
||||||
else:
|
else:
|
||||||
print(f"Unknown command: {command}")
|
print(f"Unknown command: {command}")
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
|
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
|
||||||
from pr_agent.git_providers.github_provider import GithubProvider
|
from pr_agent.git_providers.github_provider import GithubProvider
|
||||||
from pr_agent.git_providers.gitlab_provider import GitLabProvider
|
from pr_agent.git_providers.gitlab_provider import GitLabProvider
|
||||||
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
|
|
||||||
|
|
||||||
_GIT_PROVIDERS = {
|
_GIT_PROVIDERS = {
|
||||||
'github': GithubProvider,
|
'github': GithubProvider,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -10,6 +9,7 @@ from pr_agent.config_loader import settings
|
|||||||
|
|
||||||
from .git_provider import FilePatchInfo
|
from .git_provider import FilePatchInfo
|
||||||
|
|
||||||
|
|
||||||
class BitbucketProvider:
|
class BitbucketProvider:
|
||||||
def __init__(self, pr_url: Optional[str] = None):
|
def __init__(self, pr_url: Optional[str] = None):
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
@ -25,6 +25,11 @@ class BitbucketProvider:
|
|||||||
if pr_url:
|
if pr_url:
|
||||||
self.set_pr(pr_url)
|
self.set_pr(pr_url)
|
||||||
|
|
||||||
|
def is_supported(self, capability: str) -> bool:
|
||||||
|
if capability == 'get_issue_comments':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def set_pr(self, pr_url: str):
|
def set_pr(self, pr_url: str):
|
||||||
self.workspace_slug, self.repo_slug, self.pr_num = self._parse_pr_url(pr_url)
|
self.workspace_slug, self.repo_slug, self.pr_num = self._parse_pr_url(pr_url)
|
||||||
self.pr = self._get_pr()
|
self.pr = self._get_pr()
|
||||||
@ -40,7 +45,8 @@ class BitbucketProvider:
|
|||||||
for index, diff in enumerate(diffs):
|
for index, diff in enumerate(diffs):
|
||||||
original_file_content_str = self._get_pr_file_content(diff.old.get_data('links'))
|
original_file_content_str = self._get_pr_file_content(diff.old.get_data('links'))
|
||||||
new_file_content_str = self._get_pr_file_content(diff.new.get_data('links'))
|
new_file_content_str = self._get_pr_file_content(diff.new.get_data('links'))
|
||||||
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str, diff_split[index], diff.new.path))
|
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str,
|
||||||
|
diff_split[index], diff.new.path))
|
||||||
return diff_files
|
return diff_files
|
||||||
|
|
||||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||||
@ -74,6 +80,9 @@ class BitbucketProvider:
|
|||||||
def get_user_id(self):
|
def get_user_id(self):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def get_issue_comments(self):
|
||||||
|
raise NotImplementedError("Bitbucket provider does not support issue comments yet")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
|
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
|
||||||
parsed_url = urlparse(pr_url)
|
parsed_url = urlparse(pr_url)
|
||||||
|
@ -3,12 +3,15 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
|
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class EDIT_TYPE(Enum):
|
class EDIT_TYPE(Enum):
|
||||||
ADDED = 1
|
ADDED = 1
|
||||||
DELETED = 2
|
DELETED = 2
|
||||||
MODIFIED = 3
|
MODIFIED = 3
|
||||||
RENAMED = 4
|
RENAMED = 4
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FilePatchInfo:
|
class FilePatchInfo:
|
||||||
base_file: str
|
base_file: str
|
||||||
@ -21,6 +24,10 @@ class FilePatchInfo:
|
|||||||
|
|
||||||
|
|
||||||
class GitProvider(ABC):
|
class GitProvider(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def is_supported(self, capability: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_diff_files(self) -> list[FilePatchInfo]:
|
def get_diff_files(self) -> list[FilePatchInfo]:
|
||||||
pass
|
pass
|
||||||
@ -62,6 +69,10 @@ class GitProvider(ABC):
|
|||||||
def get_pr_description(self):
|
def get_pr_description(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_issue_comments(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_main_pr_language(languages, files) -> str:
|
def get_main_pr_language(languages, files) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -23,6 +23,9 @@ class GithubProvider(GitProvider):
|
|||||||
self.set_pr(pr_url)
|
self.set_pr(pr_url)
|
||||||
self.last_commit_id = list(self.pr.get_commits())[-1]
|
self.last_commit_id = list(self.pr.get_commits())[-1]
|
||||||
|
|
||||||
|
def is_supported(self, capability: str) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
def set_pr(self, pr_url: str):
|
def set_pr(self, pr_url: str):
|
||||||
self.repo, self.pr_num = self._parse_pr_url(pr_url)
|
self.repo, self.pr_num = self._parse_pr_url(pr_url)
|
||||||
self.pr = self._get_pr()
|
self.pr = self._get_pr()
|
||||||
@ -161,6 +164,9 @@ class GithubProvider(GitProvider):
|
|||||||
notifications = self.github_client.get_user().get_notifications(since=since)
|
notifications = self.github_client.get_user().get_notifications(since=since)
|
||||||
return notifications
|
return notifications
|
||||||
|
|
||||||
|
def get_issue_comments(self):
|
||||||
|
return self.pr.get_issue_comments()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
|
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
|
||||||
parsed_url = urlparse(pr_url)
|
parsed_url = urlparse(pr_url)
|
||||||
|
@ -4,10 +4,11 @@ from typing import Optional, Tuple
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import gitlab
|
import gitlab
|
||||||
|
from gitlab import GitlabGetError
|
||||||
|
|
||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
|
|
||||||
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
|
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
|
||||||
|
|
||||||
|
|
||||||
class GitLabProvider(GitProvider):
|
class GitLabProvider(GitProvider):
|
||||||
@ -31,6 +32,11 @@ class GitLabProvider(GitProvider):
|
|||||||
self.RE_HUNK_HEADER = re.compile(
|
self.RE_HUNK_HEADER = re.compile(
|
||||||
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
||||||
|
|
||||||
|
def is_supported(self, capability: str) -> bool:
|
||||||
|
if capability == 'get_issue_comments':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pr(self):
|
def pr(self):
|
||||||
'''The GitLab terminology is merge request (MR) instead of pull request (PR)'''
|
'''The GitLab terminology is merge request (MR) instead of pull request (PR)'''
|
||||||
@ -42,7 +48,12 @@ class GitLabProvider(GitProvider):
|
|||||||
self.last_diff = self.mr.diffs.list()[-1]
|
self.last_diff = self.mr.diffs.list()[-1]
|
||||||
|
|
||||||
def _get_pr_file_content(self, file_path: str, branch: str) -> str:
|
def _get_pr_file_content(self, file_path: str, branch: str) -> str:
|
||||||
return self.gl.projects.get(self.id_project).files.get(file_path, branch).decode()
|
try:
|
||||||
|
return self.gl.projects.get(self.id_project).files.get(file_path, branch).decode()
|
||||||
|
except GitlabGetError:
|
||||||
|
# In case of file creation the method returns GitlabGetError (404 file not found).
|
||||||
|
# In this case we return an empty string for the diff.
|
||||||
|
return ''
|
||||||
|
|
||||||
def get_diff_files(self) -> list[FilePatchInfo]:
|
def get_diff_files(self) -> list[FilePatchInfo]:
|
||||||
diffs = self.mr.changes()['changes']
|
diffs = self.mr.changes()['changes']
|
||||||
@ -58,8 +69,10 @@ class GitLabProvider(GitProvider):
|
|||||||
elif diff['renamed_file']:
|
elif diff['renamed_file']:
|
||||||
edit_type = EDIT_TYPE.RENAMED
|
edit_type = EDIT_TYPE.RENAMED
|
||||||
try:
|
try:
|
||||||
original_file_content_str = bytes.decode(original_file_content_str, 'utf-8')
|
if isinstance(original_file_content_str, bytes):
|
||||||
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
|
original_file_content_str = bytes.decode(original_file_content_str, 'utf-8')
|
||||||
|
if isinstance(new_file_content_str, bytes):
|
||||||
|
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}")
|
f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}")
|
||||||
@ -125,7 +138,8 @@ class GitLabProvider(GitProvider):
|
|||||||
|
|
||||||
lines = target_file.head_file.splitlines()
|
lines = target_file.head_file.splitlines()
|
||||||
relevant_line_in_file = lines[relevant_lines_start - 1]
|
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)
|
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,
|
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
|
||||||
target_file, target_line_no)
|
target_file, target_line_no)
|
||||||
|
|
||||||
@ -147,7 +161,7 @@ class GitLabProvider(GitProvider):
|
|||||||
target_file = file
|
target_file = file
|
||||||
patch = file.patch
|
patch = file.patch
|
||||||
patch_lines = patch.splitlines()
|
patch_lines = patch.splitlines()
|
||||||
for i, line in enumerate(patch_lines):
|
for line in patch_lines:
|
||||||
if line.startswith('@@'):
|
if line.startswith('@@'):
|
||||||
match = self.RE_HUNK_HEADER.match(line)
|
match = self.RE_HUNK_HEADER.match(line)
|
||||||
if not match:
|
if not match:
|
||||||
@ -203,6 +217,9 @@ class GitLabProvider(GitProvider):
|
|||||||
def get_pr_description(self):
|
def get_pr_description(self):
|
||||||
return self.mr.description
|
return self.mr.description
|
||||||
|
|
||||||
|
def get_issue_comments(self):
|
||||||
|
raise NotImplementedError("GitLab provider does not support issue comments yet")
|
||||||
|
|
||||||
def _parse_merge_request_url(self, merge_request_url: str) -> Tuple[int, int]:
|
def _parse_merge_request_url(self, merge_request_url: str) -> Tuple[int, int]:
|
||||||
parsed_url = urlparse(merge_request_url)
|
parsed_url = urlparse(merge_request_url)
|
||||||
|
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
|
|
||||||
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
|
||||||
from pr_agent.tools.pr_description import PRDescription
|
|
||||||
from pr_agent.tools.pr_questions import PRQuestions
|
|
||||||
from pr_agent.tools.pr_reviewer import PRReviewer
|
from pr_agent.tools.pr_reviewer import PRReviewer
|
||||||
|
|
||||||
|
|
||||||
@ -53,20 +50,7 @@ async def run_action():
|
|||||||
pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None)
|
pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None)
|
||||||
if pr_url:
|
if pr_url:
|
||||||
body = comment_body.strip().lower()
|
body = comment_body.strip().lower()
|
||||||
if any(cmd in body for cmd in ["/review", "/review_pr"]):
|
await PRAgent().handle_request(pr_url, body)
|
||||||
await PRReviewer(pr_url).review()
|
|
||||||
elif any(cmd in body for cmd in ["/describe", "/describe_pr"]):
|
|
||||||
await PRDescription(pr_url).describe()
|
|
||||||
elif any(cmd in body for cmd in ["/improve", "/improve_code"]):
|
|
||||||
await PRCodeSuggestions(pr_url).suggest()
|
|
||||||
elif any(cmd in body for cmd in ["/ask", "/ask_question"]):
|
|
||||||
pattern = r'(/ask|/ask_question)\s*(.*)'
|
|
||||||
matches = re.findall(pattern, comment_body, re.IGNORECASE)
|
|
||||||
if matches:
|
|
||||||
question = matches[0][1]
|
|
||||||
await PRQuestions(pr_url, question).answer()
|
|
||||||
else:
|
|
||||||
print(f"Unknown command: {body}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
@ -10,10 +9,6 @@ from pr_agent.agent.pr_agent import PRAgent
|
|||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.servers.help import bot_help_text
|
from pr_agent.servers.help import bot_help_text
|
||||||
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
|
||||||
from pr_agent.tools.pr_description import PRDescription
|
|
||||||
from pr_agent.tools.pr_questions import PRQuestions
|
|
||||||
from pr_agent.tools.pr_reviewer import PRReviewer
|
|
||||||
|
|
||||||
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
||||||
NOTIFICATION_URL = "https://api.github.com/notifications"
|
NOTIFICATION_URL = "https://api.github.com/notifications"
|
||||||
@ -103,5 +98,6 @@ async def polling_loop():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Exception during processing of a notification: {e}")
|
logging.error(f"Exception during processing of a notification: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.run(polling_loop())
|
asyncio.run(polling_loop())
|
||||||
|
@ -10,6 +10,7 @@ require_tests_review=true
|
|||||||
require_security_review=true
|
require_security_review=true
|
||||||
num_code_suggestions=3
|
num_code_suggestions=3
|
||||||
inline_code_comments = true
|
inline_code_comments = true
|
||||||
|
ask_and_reflect=false
|
||||||
|
|
||||||
[pr_description]
|
[pr_description]
|
||||||
publish_description_as_comment=false
|
publish_description_as_comment=false
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
[pr_information_from_user_prompt]
|
[pr_information_from_user_prompt]
|
||||||
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
|
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
|
||||||
Given the PR Info and the PR Git Diff, generate 4 questions about the PR for the PR author.
|
Given the PR Info and the PR Git Diff, generate 3 short questions about the PR code for the PR author.
|
||||||
The goal of the questions is to help the language model understand the PR better, so the questions should be insightful, informative, non-trivial, and relevant to the PR.
|
The goal of the questions is to help the language model understand the PR better, so the questions should be insightful, informative, non-trivial, and relevant to the PR.
|
||||||
Prefer yes\\no or multiple choice questions. If you have to ask open-ended questions, make sure they are not too difficult, and can be answered in a sentence or two.
|
You should prefer asking yes\\no questions, or multiple choice questions. Also add at least one open-ended question, but make sure they are not too difficult, and can be answered in a sentence or two.
|
||||||
|
|
||||||
|
|
||||||
Example output:
|
Example output:
|
||||||
'
|
'
|
||||||
Questions to better understand the PR:
|
Questions to better understand the PR:
|
||||||
1. ...
|
1) ...
|
||||||
2. ...
|
2) ...
|
||||||
...
|
...
|
||||||
|
'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user="""PR Info:
|
user="""PR Info:
|
||||||
|
@ -26,6 +26,12 @@ You must use the following JSON schema to format your answer:
|
|||||||
"description": "yes\\no question: does this PR have relevant tests ?"
|
"description": "yes\\no question: does this PR have relevant tests ?"
|
||||||
},
|
},
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
{%- if question_str %}
|
||||||
|
"Insights from user's answer": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "shortly summarize the insights you gained from the user's answers to the questions"
|
||||||
|
},
|
||||||
|
{%- endif %}
|
||||||
{%- if require_focused %}
|
{%- if require_focused %}
|
||||||
"Focused PR": {
|
"Focused PR": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -115,6 +121,16 @@ Description: '{{description}}'
|
|||||||
Main language: {{language}}
|
Main language: {{language}}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if question_str %}
|
||||||
|
######
|
||||||
|
Here are questions to better understand the PR. Use the answers to provide better feedback.
|
||||||
|
|
||||||
|
{{question_str|trim}}
|
||||||
|
|
||||||
|
User answers:
|
||||||
|
{{answer_str|trim}}
|
||||||
|
######
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
The PR Git Diff:
|
The PR Git Diff:
|
||||||
```
|
```
|
||||||
|
@ -8,9 +8,9 @@ from jinja2 import Environment, StrictUndefined
|
|||||||
from pr_agent.algo.ai_handler import AiHandler
|
from pr_agent.algo.ai_handler import AiHandler
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff
|
from pr_agent.algo.pr_processing import get_pr_diff
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import convert_to_markdown, try_fix_json
|
from pr_agent.algo.utils import try_fix_json
|
||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
from pr_agent.git_providers import get_git_provider, BitbucketProvider
|
from pr_agent.git_providers import BitbucketProvider, get_git_provider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
|
|
||||||
|
|
||||||
@ -62,7 +62,6 @@ class PRCodeSuggestions:
|
|||||||
logging.info('Pushing inline code comments...')
|
logging.info('Pushing inline code comments...')
|
||||||
self.push_inline_code_suggestions(data)
|
self.push_inline_code_suggestions(data)
|
||||||
|
|
||||||
|
|
||||||
async def _get_prediction(self):
|
async def _get_prediction(self):
|
||||||
variables = copy.deepcopy(self.vars)
|
variables = copy.deepcopy(self.vars)
|
||||||
variables["diff"] = self.patches_diff # update diff
|
variables["diff"] = self.patches_diff # update diff
|
||||||
@ -98,12 +97,12 @@ class PRCodeSuggestions:
|
|||||||
relevant_lines_start = int(relevant_lines_str.split('-')[0]) # absolute position
|
relevant_lines_start = int(relevant_lines_str.split('-')[0]) # absolute position
|
||||||
relevant_lines_end = int(relevant_lines_str.split('-')[-1])
|
relevant_lines_end = int(relevant_lines_str.split('-')[-1])
|
||||||
content = d['suggestion content']
|
content = d['suggestion content']
|
||||||
existing_code_snippet = d['existing code']
|
|
||||||
new_code_snippet = d['improved code']
|
new_code_snippet = d['improved code']
|
||||||
|
|
||||||
if new_code_snippet:
|
if new_code_snippet:
|
||||||
try: # dedent 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()
|
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
|
original_initial_line = None
|
||||||
for file in self.diff_files:
|
for file in self.diff_files:
|
||||||
if file.filename.strip() == relevant_file:
|
if file.filename.strip() == relevant_file:
|
||||||
@ -121,7 +120,7 @@ class PRCodeSuggestions:
|
|||||||
logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}")
|
logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}")
|
||||||
|
|
||||||
body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```"
|
body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```"
|
||||||
success = self.git_provider.publish_code_suggestion(body=body,
|
self.git_provider.publish_code_suggestion(body=body,
|
||||||
relevant_file=relevant_file,
|
relevant_file=relevant_file,
|
||||||
relevant_lines_start=relevant_lines_start,
|
relevant_lines_start=relevant_lines_start,
|
||||||
relevant_lines_end=relevant_lines_end)
|
relevant_lines_end=relevant_lines_end)
|
||||||
|
@ -7,7 +7,6 @@ from jinja2 import Environment, StrictUndefined
|
|||||||
from pr_agent.algo.ai_handler import AiHandler
|
from pr_agent.algo.ai_handler import AiHandler
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff
|
from pr_agent.algo.pr_processing import get_pr_diff
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import convert_to_markdown
|
|
||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
|
@ -35,7 +35,7 @@ class PRInformationFromUser:
|
|||||||
async def generate_questions(self):
|
async def generate_questions(self):
|
||||||
logging.info('Generating question to the user...')
|
logging.info('Generating question to the user...')
|
||||||
if settings.config.publish_output:
|
if settings.config.publish_output:
|
||||||
self.git_provider.publish_comment("Preparing answer...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing questions...", is_temporary=True)
|
||||||
logging.info('Getting PR diff...')
|
logging.info('Getting PR diff...')
|
||||||
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
|
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
|
||||||
logging.info('Getting AI prediction...')
|
logging.info('Getting AI prediction...')
|
||||||
@ -66,6 +66,6 @@ class PRInformationFromUser:
|
|||||||
model_output = self.prediction.strip()
|
model_output = self.prediction.strip()
|
||||||
if settings.config.verbosity_level >= 2:
|
if settings.config.verbosity_level >= 2:
|
||||||
logging.info(f"answer_str:\n{model_output}")
|
logging.info(f"answer_str:\n{model_output}")
|
||||||
answer_str = f"{model_output}\n\n Please respond to the question above in the following format:\n\n" + \
|
answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\
|
||||||
f"/answer <question_id> <answer>\n\n" + f"Example:\n'\n/answer\n1. Yes, because ...\n2. No, because ...\n'"
|
"\n>/answer\n>1) ...\n>2) ...\n>...\n"
|
||||||
return answer_str
|
return answer_str
|
||||||
|
@ -11,16 +11,20 @@ from pr_agent.algo.utils import convert_to_markdown, try_fix_json
|
|||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
from pr_agent.servers.help import bot_help_text, actions_help_text
|
from pr_agent.servers.help import actions_help_text, bot_help_text
|
||||||
|
|
||||||
|
|
||||||
class PRReviewer:
|
class PRReviewer:
|
||||||
def __init__(self, pr_url: str, cli_mode=False):
|
def __init__(self, pr_url: str, cli_mode=False, is_answer: bool = False):
|
||||||
|
|
||||||
self.git_provider = get_git_provider()(pr_url)
|
self.git_provider = get_git_provider()(pr_url)
|
||||||
self.main_language = get_main_pr_language(
|
self.main_language = get_main_pr_language(
|
||||||
self.git_provider.get_languages(), self.git_provider.get_files()
|
self.git_provider.get_languages(), self.git_provider.get_files()
|
||||||
)
|
)
|
||||||
|
self.is_answer = is_answer
|
||||||
|
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
|
||||||
|
raise Exception(f"Answer mode is not supported for {settings.config.git_provider} for now")
|
||||||
|
answer_str = question_str = self._get_user_answers()
|
||||||
self.ai_handler = AiHandler()
|
self.ai_handler = AiHandler()
|
||||||
self.patches_diff = None
|
self.patches_diff = None
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
@ -35,6 +39,9 @@ class PRReviewer:
|
|||||||
"require_security": settings.pr_reviewer.require_security_review,
|
"require_security": settings.pr_reviewer.require_security_review,
|
||||||
"require_focused": settings.pr_reviewer.require_focused_review,
|
"require_focused": settings.pr_reviewer.require_focused_review,
|
||||||
'num_code_suggestions': settings.pr_reviewer.num_code_suggestions,
|
'num_code_suggestions': settings.pr_reviewer.num_code_suggestions,
|
||||||
|
#
|
||||||
|
'question_str': question_str,
|
||||||
|
'answer_str': answer_str,
|
||||||
}
|
}
|
||||||
self.token_handler = TokenHandler(self.git_provider.pr,
|
self.token_handler = TokenHandler(self.git_provider.pr,
|
||||||
self.vars,
|
self.vars,
|
||||||
@ -118,9 +125,26 @@ class PRReviewer:
|
|||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
data = try_fix_json(review)
|
data = try_fix_json(review)
|
||||||
|
|
||||||
for d in data['PR Feedback']['Code suggestions']:
|
if settings.pr_reviewer.num_code_suggestions > 0:
|
||||||
relevant_file = d['relevant file'].strip()
|
try:
|
||||||
relevant_line_in_file = d['relevant line in file'].strip()
|
for d in data['PR Feedback']['Code suggestions']:
|
||||||
content = d['suggestion content']
|
relevant_file = d['relevant file'].strip()
|
||||||
|
relevant_line_in_file = d['relevant line in file'].strip()
|
||||||
|
content = d['suggestion content']
|
||||||
|
|
||||||
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
|
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_user_answers(self):
|
||||||
|
answer_str = question_str = ""
|
||||||
|
if self.is_answer:
|
||||||
|
discussion_messages = self.git_provider.get_issue_comments()
|
||||||
|
for message in discussion_messages.reversed:
|
||||||
|
if "Questions to better understand the PR:" in message.body:
|
||||||
|
question_str = message.body
|
||||||
|
elif '/answer' in message.body:
|
||||||
|
answer_str = message.body
|
||||||
|
if answer_str and question_str:
|
||||||
|
break
|
||||||
|
return question_str, answer_str
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
# Generated by CodiumAI
|
# Generated by CodiumAI
|
||||||
|
|
||||||
from pr_agent.algo.utils import try_fix_json
|
from pr_agent.algo.utils import try_fix_json
|
||||||
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
class TestTryFixJson:
|
class TestTryFixJson:
|
||||||
# Tests that JSON with complete 'Code suggestions' section returns expected output
|
# Tests that JSON with complete 'Code suggestions' section returns expected output
|
||||||
def test_incomplete_code_suggestions(self):
|
def test_incomplete_code_suggestions(self):
|
||||||
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...'
|
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...' # noqa: E501
|
||||||
expected_output = {
|
expected_output = {
|
||||||
'PR Analysis': {
|
'PR Analysis': {
|
||||||
'Main theme': 'xxx',
|
'Main theme': 'xxx',
|
||||||
@ -26,7 +25,7 @@ class TestTryFixJson:
|
|||||||
assert try_fix_json(review) == expected_output
|
assert try_fix_json(review) == expected_output
|
||||||
|
|
||||||
def test_incomplete_code_suggestions_new_line(self):
|
def test_incomplete_code_suggestions_new_line(self):
|
||||||
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n\t, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...'
|
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n\t, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...' # noqa: E501
|
||||||
expected_output = {
|
expected_output = {
|
||||||
'PR Analysis': {
|
'PR Analysis': {
|
||||||
'Main theme': 'xxx',
|
'Main theme': 'xxx',
|
||||||
@ -45,7 +44,7 @@ class TestTryFixJson:
|
|||||||
assert try_fix_json(review) == expected_output
|
assert try_fix_json(review) == expected_output
|
||||||
|
|
||||||
def test_incomplete_code_suggestions_many_close_brackets(self):
|
def test_incomplete_code_suggestions_many_close_brackets(self):
|
||||||
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy }, [}\n ,incomp.} ,..'
|
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy }, [}\n ,incomp.} ,..' # noqa: E501
|
||||||
expected_output = {
|
expected_output = {
|
||||||
'PR Analysis': {
|
'PR Analysis': {
|
||||||
'Main theme': 'xxx',
|
'Main theme': 'xxx',
|
||||||
@ -64,7 +63,7 @@ class TestTryFixJson:
|
|||||||
assert try_fix_json(review) == expected_output
|
assert try_fix_json(review) == expected_output
|
||||||
|
|
||||||
def test_incomplete_code_suggestions_relevant_file(self):
|
def test_incomplete_code_suggestions_relevant_file(self):
|
||||||
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.p'
|
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.p' # noqa: E501
|
||||||
expected_output = {
|
expected_output = {
|
||||||
'PR Analysis': {
|
'PR Analysis': {
|
||||||
'Main theme': 'xxx',
|
'Main theme': 'xxx',
|
||||||
|
Reference in New Issue
Block a user