mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-05 21:30:40 +08:00
Compare commits
9 Commits
idavidov/g
...
ok/fix_git
Author | SHA1 | Date | |
---|---|---|---|
2d5b0fa37f | |||
99f5a2ab0f | |||
d7dcecfe00 | |||
c6f8d985c2 | |||
532dfd223e | |||
6e7622822e | |||
631fb93b28 | |||
7803d8eec4 | |||
9a84b4b184 |
29
INSTALL.md
29
INSTALL.md
@ -18,6 +18,18 @@ 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:
|
||||||
|
|
||||||
@ -51,7 +63,24 @@ 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,6 +15,7 @@ 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,
|
||||||
@ -70,6 +71,8 @@ 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,14 +245,12 @@ 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('=')
|
vals = arg.split('=', 1)
|
||||||
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 = vals
|
key, value = _fix_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:
|
||||||
@ -260,6 +258,16 @@ 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,9 +14,6 @@ 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):
|
||||||
|
|
||||||
@ -59,7 +56,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 DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") from e
|
raise ValueError(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:
|
||||||
@ -153,20 +150,16 @@ 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: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int,
|
def send_inline_comment(self, body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
|
||||||
source_line_no: int, target_file: str,target_line_no: int) -> None:
|
target_file, target_line_no):
|
||||||
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:
|
||||||
# in order to have exact sha's we have to find correct diff for this change
|
d = self.last_diff
|
||||||
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': diff.base_commit_sha, 'start_sha': diff.start_commit_sha, 'head_sha': diff.head_commit_sha}
|
'base_sha': d.base_commit_sha, 'start_sha': d.start_commit_sha, 'head_sha': d.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':
|
||||||
@ -178,23 +171,6 @@ 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, "/review")
|
await agent.handle_request(api_url, "/auto_review")
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ 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, args: list = None):
|
def __init__(self, pr_url: str, is_answer: bool = False, is_auto: 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,6 +40,7 @@ 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")
|
||||||
@ -93,7 +94,11 @@ class PRReviewer:
|
|||||||
"""
|
"""
|
||||||
Review the pull request and generate feedback.
|
Review the pull request and generate feedback.
|
||||||
"""
|
"""
|
||||||
logging.info('Reviewing PR...')
|
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
||||||
|
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)
|
||||||
|
Reference in New Issue
Block a user