mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-09 07:10:37 +08:00
Compare commits
7 Commits
ok/fix_git
...
idavidov/g
Author | SHA1 | Date | |
---|---|---|---|
9770f4709a | |||
35afe758e9 | |||
50125ae57f | |||
6595c3e0c9 | |||
fdd16f6c75 | |||
7b7e913195 | |||
5477469a91 |
29
INSTALL.md
29
INSTALL.md
@ -18,18 +18,6 @@ docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> c
|
|||||||
```
|
```
|
||||||
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> ask "<your question>"
|
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> ask "<your question>"
|
||||||
```
|
```
|
||||||
Note: If you want to ensure you're running a specific version of the Docker image, consider using the image's digest.
|
|
||||||
The digest is a unique identifier for a specific version of an image. You can pull and run an image using its digest by referencing it like so: repository@sha256:digest. Always ensure you're using the correct and trusted digest for your operations.
|
|
||||||
|
|
||||||
1. To request a review for a PR using a specific digest, run the following command:
|
|
||||||
```bash
|
|
||||||
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> review
|
|
||||||
```
|
|
||||||
|
|
||||||
2. To ask a question about a PR using the same digest, run the following command:
|
|
||||||
```bash
|
|
||||||
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> ask "<your question>"
|
|
||||||
```
|
|
||||||
|
|
||||||
Possible questions you can ask include:
|
Possible questions you can ask include:
|
||||||
|
|
||||||
@ -63,24 +51,7 @@ jobs:
|
|||||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
```
|
```
|
||||||
** if you want to pin your action to a specific commit for stability reasons
|
|
||||||
```yaml
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
issue_comment:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pr_agent_job:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Run pr agent on every pull request, respond to user comments
|
|
||||||
steps:
|
|
||||||
- name: PR Agent action step
|
|
||||||
id: pragent
|
|
||||||
uses: Codium-ai/pr-agent@<commit_sha>
|
|
||||||
env:
|
|
||||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
|
||||||
2. Add the following secret to your repository under `Settings > Secrets`:
|
2. Add the following secret to your repository under `Settings > Secrets`:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -15,7 +15,6 @@ from pr_agent.tools.pr_update_changelog import PRUpdateChangelog
|
|||||||
from pr_agent.tools.pr_config import PRConfig
|
from pr_agent.tools.pr_config import PRConfig
|
||||||
|
|
||||||
command2class = {
|
command2class = {
|
||||||
"auto_review": PRReviewer,
|
|
||||||
"answer": PRReviewer,
|
"answer": PRReviewer,
|
||||||
"review": PRReviewer,
|
"review": PRReviewer,
|
||||||
"review_pr": PRReviewer,
|
"review_pr": PRReviewer,
|
||||||
@ -71,8 +70,6 @@ class PRAgent:
|
|||||||
if notify:
|
if notify:
|
||||||
notify()
|
notify()
|
||||||
await PRReviewer(pr_url, is_answer=True, args=args).run()
|
await PRReviewer(pr_url, is_answer=True, args=args).run()
|
||||||
elif action == "auto_review":
|
|
||||||
await PRReviewer(pr_url, is_auto=True, args=args).run()
|
|
||||||
elif action in command2class:
|
elif action in command2class:
|
||||||
if notify:
|
if notify:
|
||||||
notify()
|
notify()
|
||||||
|
@ -245,12 +245,14 @@ def update_settings_from_args(args: List[str]) -> List[str]:
|
|||||||
arg = arg.strip()
|
arg = arg.strip()
|
||||||
if arg.startswith('--'):
|
if arg.startswith('--'):
|
||||||
arg = arg.strip('-').strip()
|
arg = arg.strip('-').strip()
|
||||||
vals = arg.split('=', 1)
|
vals = arg.split('=')
|
||||||
if len(vals) != 2:
|
if len(vals) != 2:
|
||||||
logging.error(f'Invalid argument format: {arg}')
|
logging.error(f'Invalid argument format: {arg}')
|
||||||
other_args.append(arg)
|
other_args.append(arg)
|
||||||
continue
|
continue
|
||||||
key, value = _fix_key_value(*vals)
|
key, value = vals
|
||||||
|
key = key.strip().upper()
|
||||||
|
value = value.strip()
|
||||||
get_settings().set(key, value)
|
get_settings().set(key, value)
|
||||||
logging.info(f'Updated setting {key} to: "{value}"')
|
logging.info(f'Updated setting {key} to: "{value}"')
|
||||||
else:
|
else:
|
||||||
@ -258,16 +260,6 @@ def update_settings_from_args(args: List[str]) -> List[str]:
|
|||||||
return other_args
|
return other_args
|
||||||
|
|
||||||
|
|
||||||
def _fix_key_value(key: str, value: str):
|
|
||||||
key = key.strip().upper()
|
|
||||||
value = value.strip()
|
|
||||||
try:
|
|
||||||
value = yaml.safe_load(value)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to parse YAML for config override {key}={value}", exc_info=e)
|
|
||||||
return key, value
|
|
||||||
|
|
||||||
|
|
||||||
def load_yaml(review_text: str) -> dict:
|
def load_yaml(review_text: str) -> dict:
|
||||||
review_text = review_text.removeprefix('```yaml').rstrip('`')
|
review_text = review_text.removeprefix('```yaml').rstrip('`')
|
||||||
try:
|
try:
|
||||||
|
@ -14,6 +14,9 @@ from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
|
|||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
class DiffNotFoundError(Exception):
|
||||||
|
"""Raised when the diff for a merge request cannot be found."""
|
||||||
|
pass
|
||||||
|
|
||||||
class GitLabProvider(GitProvider):
|
class GitLabProvider(GitProvider):
|
||||||
|
|
||||||
@ -56,7 +59,7 @@ class GitLabProvider(GitProvider):
|
|||||||
self.last_diff = self.mr.diffs.list(get_all=True)[-1]
|
self.last_diff = self.mr.diffs.list(get_all=True)[-1]
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
logger.error(f"Could not get diff for merge request {self.id_mr}")
|
logger.error(f"Could not get diff for merge request {self.id_mr}")
|
||||||
raise ValueError(f"Could not get diff for merge request {self.id_mr}") from e
|
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") from e
|
||||||
|
|
||||||
|
|
||||||
def _get_pr_file_content(self, file_path: str, branch: str) -> str:
|
def _get_pr_file_content(self, file_path: str, branch: str) -> str:
|
||||||
@ -150,16 +153,20 @@ class GitLabProvider(GitProvider):
|
|||||||
def create_inline_comments(self, comments: list[dict]):
|
def create_inline_comments(self, comments: list[dict]):
|
||||||
raise NotImplementedError("Gitlab provider does not support publishing inline comments yet")
|
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,
|
def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int,
|
||||||
target_file, target_line_no):
|
source_line_no: int, target_file: str,target_line_no: int) -> None:
|
||||||
if not found:
|
if not found:
|
||||||
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
||||||
else:
|
else:
|
||||||
d = self.last_diff
|
# in order to have exact sha's we have to find correct diff for this change
|
||||||
|
diff = self.get_relevant_diff(relevant_file, relevant_line_in_file)
|
||||||
|
if diff is None:
|
||||||
|
logger.error(f"Could not get diff for merge request {self.id_mr}")
|
||||||
|
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}")
|
||||||
pos_obj = {'position_type': 'text',
|
pos_obj = {'position_type': 'text',
|
||||||
'new_path': target_file.filename,
|
'new_path': target_file.filename,
|
||||||
'old_path': target_file.old_filename if target_file.old_filename else target_file.filename,
|
'old_path': target_file.old_filename if target_file.old_filename else target_file.filename,
|
||||||
'base_sha': d.base_commit_sha, 'start_sha': d.start_commit_sha, 'head_sha': d.head_commit_sha}
|
'base_sha': diff.base_commit_sha, 'start_sha': diff.start_commit_sha, 'head_sha': diff.head_commit_sha}
|
||||||
if edit_type == 'deletion':
|
if edit_type == 'deletion':
|
||||||
pos_obj['old_line'] = source_line_no - 1
|
pos_obj['old_line'] = source_line_no - 1
|
||||||
elif edit_type == 'addition':
|
elif edit_type == 'addition':
|
||||||
@ -171,6 +178,23 @@ class GitLabProvider(GitProvider):
|
|||||||
self.mr.discussions.create({'body': body,
|
self.mr.discussions.create({'body': body,
|
||||||
'position': pos_obj})
|
'position': pos_obj})
|
||||||
|
|
||||||
|
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]:
|
||||||
|
changes = self.mr.changes() # Retrieve the changes for the merge request once
|
||||||
|
if not changes:
|
||||||
|
logging.error('No changes found for the merge request.')
|
||||||
|
return None
|
||||||
|
all_diffs = self.mr.diffs.list(get_all=True)
|
||||||
|
if not all_diffs:
|
||||||
|
logging.error('No diffs found for the merge request.')
|
||||||
|
return None
|
||||||
|
for diff in all_diffs:
|
||||||
|
for change in changes['changes']:
|
||||||
|
if change['new_path'] == relevant_file and relevant_line_in_file in change['diff']:
|
||||||
|
return diff
|
||||||
|
logging.debug(
|
||||||
|
f'No relevant diff found for {relevant_file} {relevant_line_in_file}. Falling back to last diff.')
|
||||||
|
return self.last_diff # fallback to last_diff if no relevant diff is found
|
||||||
|
|
||||||
def publish_code_suggestions(self, code_suggestions: list):
|
def publish_code_suggestions(self, code_suggestions: list):
|
||||||
for suggestion in code_suggestions:
|
for suggestion in code_suggestions:
|
||||||
try:
|
try:
|
||||||
|
@ -93,7 +93,7 @@ async def handle_request(body: Dict[str, Any]):
|
|||||||
api_url = pull_request.get("url")
|
api_url = pull_request.get("url")
|
||||||
if not api_url:
|
if not api_url:
|
||||||
return {}
|
return {}
|
||||||
await agent.handle_request(api_url, "/auto_review")
|
await agent.handle_request(api_url, "/review")
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ require_security_review=true
|
|||||||
num_code_suggestions=3
|
num_code_suggestions=3
|
||||||
inline_code_comments = false
|
inline_code_comments = false
|
||||||
ask_and_reflect=false
|
ask_and_reflect=false
|
||||||
automatic_review=true
|
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
|
|
||||||
[pr_description] # /describe #
|
[pr_description] # /describe #
|
||||||
|
@ -23,7 +23,7 @@ class PRReviewer:
|
|||||||
"""
|
"""
|
||||||
The PRReviewer class is responsible for reviewing a pull request and generating feedback using an AI model.
|
The PRReviewer class is responsible for reviewing a pull request and generating feedback using an AI model.
|
||||||
"""
|
"""
|
||||||
def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False, args: list = None):
|
def __init__(self, pr_url: str, is_answer: bool = False, args: list = None):
|
||||||
"""
|
"""
|
||||||
Initialize the PRReviewer object with the necessary attributes and objects to review a pull request.
|
Initialize the PRReviewer object with the necessary attributes and objects to review a pull request.
|
||||||
|
|
||||||
@ -40,7 +40,6 @@ class PRReviewer:
|
|||||||
)
|
)
|
||||||
self.pr_url = pr_url
|
self.pr_url = pr_url
|
||||||
self.is_answer = is_answer
|
self.is_answer = is_answer
|
||||||
self.is_auto = is_auto
|
|
||||||
|
|
||||||
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
|
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
|
||||||
raise Exception(f"Answer mode is not supported for {get_settings().config.git_provider} for now")
|
raise Exception(f"Answer mode is not supported for {get_settings().config.git_provider} for now")
|
||||||
@ -94,12 +93,8 @@ class PRReviewer:
|
|||||||
"""
|
"""
|
||||||
Review the pull request and generate feedback.
|
Review the pull request and generate feedback.
|
||||||
"""
|
"""
|
||||||
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
logging.info('Reviewing PR...')
|
||||||
logging.info(f'Automatic review is disabled {self.pr_url}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
logging.info(f'Reviewing PR: {self.pr_url} ...')
|
|
||||||
|
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
|
||||||
|
|
||||||
|
@ -14,4 +14,4 @@ GitPython~=3.1.32
|
|||||||
litellm~=0.1.351
|
litellm~=0.1.351
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
starlette-context==0.3.6
|
starlette-context==0.3.6
|
||||||
litellm~=0.1.351
|
litellm~=0.1.351
|
Reference in New Issue
Block a user