mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-03 20:30:41 +08:00
Compare commits
62 Commits
ok/json_lo
...
ok/bitbuck
Author | SHA1 | Date | |
---|---|---|---|
c0b3c76884 | |||
e1370a8385 | |||
d0f3a4139d | |||
3ddc7e79d1 | |||
3e14edfd4e | |||
15573e2286 | |||
ce64877063 | |||
6666a128ee | |||
9fbf89670d | |||
ad1c51c536 | |||
9ab7ccd20d | |||
c907f93ab8 | |||
29a8cf8357 | |||
7b6a6c7164 | |||
a751bb0ef0 | |||
26d6280a20 | |||
32a19fdab6 | |||
775ccb3f25 | |||
a1c6c57f7b | |||
73bb70fef4 | |||
dcac6c145c | |||
4bda9dfe04 | |||
66644f0224 | |||
e74bb80668 | |||
e06fb534d3 | |||
71a341855e | |||
7d949ad6e2 | |||
4b5f86fcf0 | |||
cd11f51df0 | |||
b40c0b9b23 | |||
816ddeeb9e | |||
11f01a226c | |||
b57ec301e8 | |||
71da20ea7e | |||
c895657310 | |||
eda20ccca9 | |||
aed113cd79 | |||
0ab07a46c6 | |||
5f32e28933 | |||
7538c4dd2f | |||
e3845283f8 | |||
a85921d3c5 | |||
27b64fbcaf | |||
8d50f2ae82 | |||
e97a03f522 | |||
2e3344b5b0 | |||
e1b51eace7 | |||
49e3d5ec5f | |||
afa78ed3fb | |||
72d5e4748e | |||
61d3e1ebf4 | |||
055b5ea700 | |||
3434296792 | |||
ae375c2ff0 | |||
3d5efdf4f3 | |||
e83747300d | |||
013a689b33 | |||
e6bea76eee | |||
414f2b6767 | |||
6541575a0e | |||
02570ea797 | |||
65bb70a1dd |
@ -368,7 +368,7 @@ PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
|
||||
```
|
||||
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
|
||||
```
|
||||
3. Follow the instructions to build the Docker image, setup a secrets file and deploy on your own server from [Method 5](#method-5-run-as-a-github-app) steps 4-7.
|
||||
3. Follow the instructions to build the Docker image, setup a secrets file and deploy on your own server from [Method 5](#run-as-a-github-app) steps 4-7.
|
||||
4. In the secrets file, fill in the following:
|
||||
- Your OpenAI key.
|
||||
- In the [gitlab] section, fill in personal_access_token and shared_secret. The access token can be a personal access token, or a group or project access token.
|
||||
|
@ -31,6 +31,8 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull
|
||||
‣ **Find Similar Issue ([`/similar_issue`](./docs/SIMILAR_ISSUE.md))**: Automatically retrieves and presents similar issues
|
||||
\
|
||||
‣ **Add Documentation ([`/add_docs`](./docs/ADD_DOCUMENTATION.md))**: Automatically adds documentation to un-documented functions/classes in the PR.
|
||||
\
|
||||
‣ **Generate Custom Labels ([`/generate_labels`](./docs/GENERATE_CUSTOM_LABELS.md))**: Automatically suggests custom labels based on the PR code changes.
|
||||
|
||||
See the [Installation Guide](./INSTALL.md) for instructions how to install and run the tool on different platforms.
|
||||
|
||||
@ -115,6 +117,7 @@ See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the dif
|
||||
| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
|
||||
| | Find similar issue | :white_check_mark: | | | | | |
|
||||
| | Add Documentation | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
|
||||
| | Generate Labels | :white_check_mark: | :white_check_mark: | | | | |
|
||||
| | | | | | | |
|
||||
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| | App / webhook | :white_check_mark: | :white_check_mark: | | | |
|
||||
|
@ -1,3 +1,28 @@
|
||||
## [Version 0.9] - 2023-10-29
|
||||
- codiumai/pr-agent:0.9
|
||||
- codiumai/pr-agent:0.9-github_app
|
||||
- codiumai/pr-agent:0.9-bitbucket-app
|
||||
- codiumai/pr-agent:0.9-gitlab_webhook
|
||||
- codiumai/pr-agent:0.9-github_polling
|
||||
- codiumai/pr-agent:0.9-github_action
|
||||
|
||||
### Added::Algo
|
||||
- New tool - [generate_labels](https://github.com/Codium-ai/pr-agent/blob/main/docs/GENERATE_CUSTOM_LABELS.md)
|
||||
- New ability to use [customize labels](https://github.com/Codium-ai/pr-agent/blob/main/docs/GENERATE_CUSTOM_LABELS.md#how-to-enable-custom-labels) on the `review` and `describe` tools.
|
||||
- New tool - [add_docs](https://github.com/Codium-ai/pr-agent/blob/main/docs/ADD_DOCUMENTATION.md)
|
||||
- GitHub Action: Can now use a `.pr_agent.toml` file to control configuration parameters (see [Usage Guide](./Usage.md#working-with-github-action)).
|
||||
- GitHub App: Added ability to trigger tools on [push events](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools-for-new-code-pr-push)
|
||||
- Support custom domain URLs for Azure devops integration (see [link](https://github.com/Codium-ai/pr-agent/pull/381)).
|
||||
- PR Description default mode is now in [bullet points](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L35).
|
||||
|
||||
### Added::Documentation
|
||||
Significant documentation updates (see [Installation Guide](https://github.com/Codium-ai/pr-agent/blob/main/INSTALL.md), [Usage Guide](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md), and [Tools Guide](https://github.com/Codium-ai/pr-agent/blob/main/docs/TOOLS_GUIDE.md))
|
||||
|
||||
### Fixed
|
||||
- Fixed support for BitBucket pipeline (see [link](https://github.com/Codium-ai/pr-agent/pull/386))
|
||||
- Fixed a bug in `review -i` tool
|
||||
- Added blacklist for specific file extensions in `add_docs` tool (see [link](https://github.com/Codium-ai/pr-agent/pull/385/))
|
||||
|
||||
## [Version 0.8] - 2023-09-27
|
||||
- codiumai/pr-agent:0.8
|
||||
- codiumai/pr-agent:0.8-github_app
|
||||
|
77
Usage.md
77
Usage.md
@ -108,19 +108,41 @@ Any configuration value in [configuration file](pr_agent/settings/configuration.
|
||||
|
||||
|
||||
### Working with GitHub App
|
||||
When running PR-Agent from [GitHub App](INSTALL.md#method-5-run-as-a-github-app), the default configurations from a pre-built docker will be initially loaded.
|
||||
When running PR-Agent from GitHub App, the default [configuration file](pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded.
|
||||
|
||||
By uploading a local `.pr_agent.toml` file, you can edit and customize any configuration parameter.
|
||||
|
||||
For example, if you set in `.pr_agent.toml`:
|
||||
|
||||
```
|
||||
[pr_reviewer]
|
||||
num_code_suggestions=1
|
||||
```
|
||||
|
||||
Than you will overwrite the default number of code suggestions to be 1.
|
||||
|
||||
#### GitHub app automatic tools
|
||||
The [github_app](pr_agent/settings/configuration.toml#L56) section defines GitHub app specific configurations.
|
||||
An important parameter is `pr_commands`, which is a list of tools that will be **run automatically** when a new PR is opened:
|
||||
The [github_app](pr_agent/settings/configuration.toml#L76) section defines GitHub app-specific configurations.
|
||||
In this section you can define configurations to control the conditions for which tools will **run automatically**.
|
||||
|
||||
##### GitHub app automatic tools for PR actions
|
||||
The GitHub app can respond to the following actions on a PR:
|
||||
1. `opened` - Opening a new PR
|
||||
2. `reopened` - Reopening a closed PR
|
||||
3. `ready_for_review` - Moving a PR from Draft to Open
|
||||
4. `review_requested` - Specifically requesting review (in the PR reviewers list) from the `github-actions[bot]` user
|
||||
|
||||
The configuration parameter `handle_pr_actions` defines the list of actions for which the GitHub app will trigger the PR-Agent.
|
||||
The configuration parameter `pr_commands` defines the list of tools that will be **run automatically** when one of the above action happens (e.g. a new PR is opened):
|
||||
```
|
||||
[github_app]
|
||||
handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested']
|
||||
pr_commands = [
|
||||
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
||||
"/auto_review",
|
||||
]
|
||||
```
|
||||
This means that when a new PR is opened, PR-Agent will run the `describe` and `auto_review` tools.
|
||||
This means that when a new PR is opened/reopened or marked as ready for review, PR-Agent will run the `describe` and `auto_review` tools.
|
||||
For the describe tool, the `add_original_user_description` and `keep_original_user_title` parameters will be set to true.
|
||||
|
||||
You can override the default tool parameters by uploading a local configuration file called `.pr_agent.toml` to the root of your repo.
|
||||
@ -135,11 +157,27 @@ When a new PR is opened, PR-Agent will run the `describe` tool with the above pa
|
||||
To cancel the automatic run of all the tools, set:
|
||||
```
|
||||
[github_app]
|
||||
pr_commands = ""
|
||||
handle_pr_actions = []
|
||||
```
|
||||
|
||||
##### GitHub app automatic tools for new code (PR push)
|
||||
In addition to running automatic tools when a PR is opened, the GitHub app can also respond to new code that is pushed to an open PR.
|
||||
|
||||
Note that a local `.pr_agent.toml` file enables you to edit and customize the default parameters of any tool, not just the ones that are run automatically.
|
||||
The configuration toggle `handle_push_trigger` can be used to enable this feature.
|
||||
The configuration parameter `push_commands` defines the list of tools that will be **run automatically** when new code is pushed to the PR.
|
||||
```
|
||||
[github_app]
|
||||
handle_push_trigger = true
|
||||
push_commands = [
|
||||
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
||||
"/auto_review -i --pr_reviewer.remove_previous_review_comment=true",
|
||||
]
|
||||
```
|
||||
The means that when new code is pused to the PR, the PR-Agent will run the `describe` and incremental `auto_review` tools.
|
||||
For the describe tool, the `add_original_user_description` and `keep_original_user_title` parameters will be set to true.
|
||||
For the `auto_review` tool, it will run in incremental mode, and the `remove_previous_review_comment` parameter will be set to true.
|
||||
|
||||
Much like the configurations for `pr_commands`, you can override the default tool paramteres by uploading a local configuration file to the root of your repo.
|
||||
|
||||
#### Editing the prompts
|
||||
The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder.
|
||||
@ -159,21 +197,28 @@ user="""
|
||||
Note that the new prompt will need to generate an output compatible with the relevant [post-process function](./pr_agent/tools/pr_description.py#L137).
|
||||
|
||||
### Working with GitHub Action
|
||||
You can configure settings in GitHub action by adding environment variables under the env section in `.github/workflows/pr_agent.yml` file. Some examples:
|
||||
You can configure settings in GitHub action by adding environment variables under the env section in `.github/workflows/pr_agent.yml` file.
|
||||
Specifically, start by setting the following environment variables:
|
||||
```yaml
|
||||
env:
|
||||
# ... previous environment values
|
||||
OPENAI.ORG: "<Your organization name under your OpenAI account>"
|
||||
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false" # Disable tests review
|
||||
PR_CODE_SUGGESTIONS.NUM_CODE_SUGGESTIONS: 6 # Increase number of code suggestions
|
||||
github_action.auto_review: "true" # Enable auto review
|
||||
github_action.auto_describe: "true" # Enable auto describe
|
||||
github_action.auto_improve: "false" # Disable auto improve
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }} # Make sure to add your OpenAI key to your repo secrets
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Make sure to add your GitHub token to your repo secrets
|
||||
github_action.auto_review: "true" # enable\disable auto review
|
||||
github_action.auto_describe: "true" # enable\disable auto describe
|
||||
github_action.auto_improve: "false" # enable\disable auto improve
|
||||
```
|
||||
specifically, `github_action.auto_review`, `github_action.auto_describe` and `github_action.auto_improve` are used to enable/disable automatic tools that run when a new PR is opened.
|
||||
|
||||
`github_action.auto_review`, `github_action.auto_describe` and `github_action.auto_improve` are used to enable/disable automatic tools that run when a new PR is opened.
|
||||
If not set, the default option is that only the `review` tool will run automatically when a new PR is opened.
|
||||
|
||||
Note that you can give additional config parameters by adding environment variables to `.github/workflows/pr_agent.yml`, or by using a `.pr_agent.toml` file in the root of your repo, similar to the GitHub App usage.
|
||||
|
||||
For example, you can set an environment variable: `pr_description.add_original_user_description=false`, or add a `.pr_agent.toml` file with the following content:
|
||||
```
|
||||
[pr_description]
|
||||
add_original_user_description = false
|
||||
```
|
||||
|
||||
|
||||
### Changing a model
|
||||
|
||||
See [here](pr_agent/algo/__init__.py) for the list of available models.
|
||||
|
@ -26,7 +26,7 @@ Under the section 'pr_description', the [configuration file](./../pr_agent/setti
|
||||
- `keep_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is false.
|
||||
|
||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
||||
|
||||
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
|
||||
### Markers template
|
||||
|
||||
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Generate Custom Labels
|
||||
The `generte_labels` tool scans the PR code changes, and given a list of labels and their descriptions, it automatically suggests labels that match the PR code changes.
|
||||
The `generate_labels` tool scans the PR code changes, and given a list of labels and their descriptions, it automatically suggests labels that match the PR code changes.
|
||||
|
||||
It can be invoked manually by commenting on any PR:
|
||||
```
|
||||
@ -10,26 +10,32 @@ For example:
|
||||
If we wish to add detect changes to SQL queries in a given PR, we can add the following custom label along with its description:
|
||||
|
||||
<kbd><img src=./../pics/custom_labels_list.png width="768"></kbd>
|
||||
When running the `generte_labels` tool on a PR that includes changes in SQL queries, it will automatically suggest the custom label:
|
||||
|
||||
When running the `generate_labels` tool on a PR that includes changes in SQL queries, it will automatically suggest the custom label:
|
||||
<kbd><img src=./../pics/custom_label_published.png width="768"></kbd>
|
||||
|
||||
### Configuration options
|
||||
To enable custom labels, you need to add the following configuration to the [custom_labels file](./../pr_agent/settings/custom_labels.toml):
|
||||
### How to enable custom labels
|
||||
|
||||
Note that in addition to the dedicated tool `generate_labels`, the custom labels will also be used by the `review` and `describe` tools.
|
||||
|
||||
#### CLI
|
||||
To enable custom labels, you need to apply the [configuration changes](#configuration-changes) to the [custom_labels file](./../pr_agent/settings/custom_labels.toml):
|
||||
|
||||
#### GitHub Action and GitHub App
|
||||
To enable custom labels, you need to apply the [configuration changes](#configuration-changes) to the local `.pr_agent.toml` file in you repository.
|
||||
|
||||
#### Configuration changes
|
||||
- Change `enable_custom_labels` to True: This will turn off the default labels and enable the custom labels provided in the custom_labels.toml file.
|
||||
- Add the custom labels to the custom_labels.toml file. It should be formatted as follows:
|
||||
```
|
||||
- Add the custom labels. It should be formatted as follows:
|
||||
|
||||
```
|
||||
[config]
|
||||
enable_custom_labels=true
|
||||
|
||||
[custom_labels."Custom Label Name"]
|
||||
description = "Description of when AI should suggest this label"
|
||||
```
|
||||
- You can add modify the list to include all the custom labels you wish to use in your repository.
|
||||
|
||||
#### Github Action
|
||||
To use the `generte_labels` tool with Github Action:
|
||||
|
||||
- Add the following file to your repository under `env` section in `.github/workflows/pr_agent.yml`
|
||||
- Comma separated list of custom labels and their descriptions
|
||||
- The number of labels and descriptions should be the same and in the same order (empty descriptions are allowed):
|
||||
[custom_labels."Custom Label 2"]
|
||||
description = "Description of when AI should suggest this label 2"
|
||||
```
|
||||
CUSTOM_LABELS: "label1, label2, ..."
|
||||
CUSTOM_LABELS_DESCRIPTION: "label1 description, label2 description, ..."
|
||||
```
|
||||
|
||||
|
@ -25,6 +25,7 @@ Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings
|
||||
- `inline_code_comments`: if set to true, the tool will publish the code suggestions as comments on the code diff. Default is false.
|
||||
- `automatic_review`: if set to false, no automatic reviews will be done. Default is true.
|
||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
||||
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
|
||||
#### Incremental Mode
|
||||
For an incremental review, which only considers changes since the last PR-Agent review, this can be useful when working on the PR in an iterative manner, and you want to focus on the changes since the last review instead of reviewing the entire PR again, the following command can be used:
|
||||
```
|
||||
|
@ -6,5 +6,6 @@
|
||||
- [SIMILAR_ISSUE](./SIMILAR_ISSUE.md)
|
||||
- [UPDATE CHANGELOG](./UPDATE_CHANGELOG.md)
|
||||
- [ADD DOCUMENTATION](./ADD_DOCUMENTATION.md)
|
||||
- [GENERATE CUSTOM LABELS](./GENERATE_CUSTOM_LABELS.md)
|
||||
|
||||
See the **[installation guide](/INSTALL.md)** for instructions on how to setup PR-Agent.
|
@ -101,7 +101,8 @@ def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> s
|
||||
markdown_text += f" **{sub_key}:** {sub_value}\n"
|
||||
if not gfm_supported:
|
||||
if "relevant line" not in sub_key.lower(): # nicer presentation
|
||||
markdown_text = markdown_text.rstrip('\n') + "\\\n"
|
||||
# markdown_text = markdown_text.rstrip('\n') + "\\\n" # works for gitlab
|
||||
markdown_text = markdown_text.rstrip('\n') + " \n" # works for gitlab and bitbucker
|
||||
|
||||
markdown_text += "\n"
|
||||
return markdown_text
|
||||
@ -307,6 +308,9 @@ def try_fix_yaml(review_text: str) -> dict:
|
||||
|
||||
|
||||
def set_custom_labels(variables):
|
||||
if not get_settings().config.enable_custom_labels:
|
||||
return
|
||||
|
||||
labels = get_settings().custom_labels
|
||||
if not labels:
|
||||
# set default labels
|
||||
|
@ -41,9 +41,10 @@ class BitbucketProvider(GitProvider):
|
||||
|
||||
def get_repo_settings(self):
|
||||
try:
|
||||
contents = self.repo_obj.get_contents(
|
||||
".pr_agent.toml", ref=self.pr.head.sha
|
||||
).decoded_content
|
||||
url = (f"https://api.bitbucket.org/2.0/repositories/{self.workspace_slug}/{self.repo_slug}/src/"
|
||||
f"{self.pr.destination_branch}/.pr_agent.toml")
|
||||
response = requests.request("GET", url, headers=self.headers)
|
||||
contents = response.text.encode('utf-8')
|
||||
return contents
|
||||
except Exception:
|
||||
return ""
|
||||
@ -142,10 +143,15 @@ class BitbucketProvider(GitProvider):
|
||||
def remove_initial_comment(self):
|
||||
try:
|
||||
for comment in self.temp_comments:
|
||||
self.pr.delete(f"comments/{comment}")
|
||||
self.remove_comment(comment)
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to remove temp comments, error: {e}")
|
||||
|
||||
def remove_comment(self, comment):
|
||||
try:
|
||||
self.pr.delete(f"comments/{comment}")
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to remove comment, error: {e}")
|
||||
|
||||
# funtion to create_inline_comment
|
||||
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||
@ -177,7 +183,7 @@ class BitbucketProvider(GitProvider):
|
||||
|
||||
def publish_inline_comments(self, comments: list[dict]):
|
||||
for comment in comments:
|
||||
self.publish_inline_comment(comment['body'], comment['start_line'], comment['path'])
|
||||
self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
|
||||
|
||||
def get_title(self):
|
||||
return self.pr.title
|
||||
|
@ -221,6 +221,9 @@ class CodeCommitProvider(GitProvider):
|
||||
def remove_initial_comment(self):
|
||||
return "" # not implemented yet
|
||||
|
||||
def remove_comment(self, comment):
|
||||
return "" # not implemented yet
|
||||
|
||||
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html
|
||||
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
|
||||
|
@ -396,5 +396,8 @@ class GerritProvider(GitProvider):
|
||||
# shutil.rmtree(self.repo_path)
|
||||
pass
|
||||
|
||||
def remove_comment(self, comment):
|
||||
pass
|
||||
|
||||
def get_pr_branch(self):
|
||||
return self.repo.head
|
||||
|
@ -71,6 +71,10 @@ class GitProvider(ABC):
|
||||
def remove_initial_comment(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_comment(self, comment):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_languages(self):
|
||||
pass
|
||||
@ -139,6 +143,9 @@ def get_main_pr_language(languages, files) -> str:
|
||||
if not languages:
|
||||
get_logger().info("No languages detected")
|
||||
return main_language_str
|
||||
if not files:
|
||||
get_logger().info("No files in diff")
|
||||
return main_language_str
|
||||
|
||||
try:
|
||||
top_language = max(languages, key=languages.get).lower()
|
||||
|
@ -50,7 +50,7 @@ class GithubProvider(GitProvider):
|
||||
def get_incremental_commits(self):
|
||||
self.commits = list(self.pr.get_commits())
|
||||
|
||||
self.get_previous_review()
|
||||
self.previous_review = self.get_previous_review(full=True, incremental=True)
|
||||
if self.previous_review:
|
||||
self.incremental.commits_range = self.get_commit_range()
|
||||
# Get all files changed during the commit range
|
||||
@ -63,7 +63,7 @@ class GithubProvider(GitProvider):
|
||||
|
||||
def get_commit_range(self):
|
||||
last_review_time = self.previous_review.created_at
|
||||
first_new_commit_index = 0
|
||||
first_new_commit_index = None
|
||||
for index in range(len(self.commits) - 1, -1, -1):
|
||||
if self.commits[index].commit.author.date > last_review_time:
|
||||
self.incremental.first_new_commit_sha = self.commits[index].sha
|
||||
@ -71,15 +71,21 @@ class GithubProvider(GitProvider):
|
||||
else:
|
||||
self.incremental.last_seen_commit_sha = self.commits[index].sha
|
||||
break
|
||||
return self.commits[first_new_commit_index:]
|
||||
return self.commits[first_new_commit_index:] if first_new_commit_index is not None else []
|
||||
|
||||
def get_previous_review(self):
|
||||
self.previous_review = None
|
||||
self.comments = list(self.pr.get_issue_comments())
|
||||
def get_previous_review(self, *, full: bool, incremental: bool):
|
||||
if not (full or incremental):
|
||||
raise ValueError("At least one of full or incremental must be True")
|
||||
if not getattr(self, "comments", None):
|
||||
self.comments = list(self.pr.get_issue_comments())
|
||||
prefixes = []
|
||||
if full:
|
||||
prefixes.append("## PR Analysis")
|
||||
if incremental:
|
||||
prefixes.append("## Incremental PR Review")
|
||||
for index in range(len(self.comments) - 1, -1, -1):
|
||||
if self.comments[index].body.startswith("## PR Analysis") or self.comments[index].body.startswith("## Incremental PR Review"):
|
||||
self.previous_review = self.comments[index]
|
||||
break
|
||||
if any(self.comments[index].body.startswith(prefix) for prefix in prefixes):
|
||||
return self.comments[index]
|
||||
|
||||
def get_files(self):
|
||||
if self.incremental.is_incremental and self.file_set:
|
||||
@ -218,10 +224,16 @@ class GithubProvider(GitProvider):
|
||||
try:
|
||||
for comment in getattr(self.pr, 'comments_list', []):
|
||||
if comment.is_temporary:
|
||||
comment.delete()
|
||||
self.remove_comment(comment)
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to remove initial comment, error: {e}")
|
||||
|
||||
def remove_comment(self, comment):
|
||||
try:
|
||||
comment.delete()
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to remove comment, error: {e}")
|
||||
|
||||
def get_title(self):
|
||||
return self.pr.title
|
||||
|
||||
|
@ -287,10 +287,16 @@ class GitLabProvider(GitProvider):
|
||||
def remove_initial_comment(self):
|
||||
try:
|
||||
for comment in self.temp_comments:
|
||||
comment.delete()
|
||||
self.remove_comment(comment)
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to remove temp comments, error: {e}")
|
||||
|
||||
def remove_comment(self, comment):
|
||||
try:
|
||||
comment.delete()
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to remove comment, error: {e}")
|
||||
|
||||
def get_title(self):
|
||||
return self.mr.title
|
||||
|
||||
@ -309,7 +315,7 @@ class GitLabProvider(GitProvider):
|
||||
|
||||
def get_repo_settings(self):
|
||||
try:
|
||||
contents = self.gl.projects.get(self.id_project).files.get(file_path='.pr_agent.toml', ref=self.mr.source_branch)
|
||||
contents = self.gl.projects.get(self.id_project).files.get(file_path='.pr_agent.toml', ref=self.mr.target_branch).decode()
|
||||
return contents
|
||||
except Exception:
|
||||
return ""
|
||||
|
@ -140,6 +140,9 @@ class LocalGitProvider(GitProvider):
|
||||
def remove_initial_comment(self):
|
||||
pass # Not applicable to the local git provider, but required by the interface
|
||||
|
||||
def remove_comment(self, comment):
|
||||
pass # Not applicable to the local git provider, but required by the interface
|
||||
|
||||
def get_languages(self):
|
||||
"""
|
||||
Calculate percentage of languages in repository. Used for hunk prioritisation.
|
||||
|
@ -19,10 +19,6 @@ async def run_action():
|
||||
OPENAI_KEY = os.environ.get('OPENAI_KEY') or os.environ.get('OPENAI.KEY')
|
||||
OPENAI_ORG = os.environ.get('OPENAI_ORG') or os.environ.get('OPENAI.ORG')
|
||||
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
|
||||
CUSTOM_LABELS = os.environ.get('CUSTOM_LABELS')
|
||||
CUSTOM_LABELS_DESCRIPTIONS = os.environ.get('CUSTOM_LABELS_DESCRIPTIONS')
|
||||
# CUSTOM_LABELS is a comma separated list of labels (string), convert to list and strip spaces
|
||||
|
||||
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
|
||||
|
||||
# Check if required environment variables are set
|
||||
@ -38,7 +34,6 @@ async def run_action():
|
||||
if not GITHUB_TOKEN:
|
||||
print("GITHUB_TOKEN not set")
|
||||
return
|
||||
# CUSTOM_LABELS_DICT = handle_custom_labels(CUSTOM_LABELS, CUSTOM_LABELS_DESCRIPTIONS)
|
||||
|
||||
# Set the environment variables in the settings
|
||||
get_settings().set("OPENAI.KEY", OPENAI_KEY)
|
||||
@ -46,7 +41,6 @@ async def run_action():
|
||||
get_settings().set("OPENAI.ORG", OPENAI_ORG)
|
||||
get_settings().set("GITHUB.USER_TOKEN", GITHUB_TOKEN)
|
||||
get_settings().set("GITHUB.DEPLOYMENT_TYPE", "user")
|
||||
# get_settings().set("CUSTOM_LABELS", CUSTOM_LABELS_DICT)
|
||||
|
||||
# Load the event payload
|
||||
try:
|
||||
@ -104,31 +98,5 @@ async def run_action():
|
||||
await PRAgent().handle_request(url, body)
|
||||
|
||||
|
||||
def handle_custom_labels(CUSTOM_LABELS, CUSTOM_LABELS_DESCRIPTIONS):
|
||||
if CUSTOM_LABELS:
|
||||
CUSTOM_LABELS = [x.strip() for x in CUSTOM_LABELS.split(',')]
|
||||
else:
|
||||
# Set default labels
|
||||
CUSTOM_LABELS = ['Bug fix', 'Tests', 'Bug fix with tests', 'Refactoring', 'Enhancement', 'Documentation',
|
||||
'Other']
|
||||
print(f"Using default labels: {CUSTOM_LABELS}")
|
||||
if CUSTOM_LABELS_DESCRIPTIONS:
|
||||
CUSTOM_LABELS_DESCRIPTIONS = [x.strip() for x in CUSTOM_LABELS_DESCRIPTIONS.split(',')]
|
||||
else:
|
||||
# Set default labels
|
||||
CUSTOM_LABELS_DESCRIPTIONS = ['Fixes a bug in the code', 'Adds or modifies tests',
|
||||
'Fixes a bug in the code and adds or modifies tests',
|
||||
'Refactors the code without changing its functionality',
|
||||
'Adds new features or functionality',
|
||||
'Adds or modifies documentation',
|
||||
'Other changes that do not fit in any of the above categories']
|
||||
print(f"Using default labels: {CUSTOM_LABELS_DESCRIPTIONS}")
|
||||
# create a dictionary of labels and descriptions
|
||||
CUSTOM_LABELS_DICT = dict()
|
||||
for i in range(len(CUSTOM_LABELS)):
|
||||
CUSTOM_LABELS_DICT[CUSTOM_LABELS[i]] = {'description': CUSTOM_LABELS_DESCRIPTIONS[i]}
|
||||
return CUSTOM_LABELS_DICT
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(run_action())
|
@ -1,7 +1,7 @@
|
||||
import copy
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
import asyncio.locks
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import uvicorn
|
||||
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
|
||||
@ -14,8 +14,9 @@ from pr_agent.algo.utils import update_settings_from_args
|
||||
from pr_agent.config_loader import get_settings, global_settings
|
||||
from pr_agent.git_providers import get_git_provider
|
||||
from pr_agent.git_providers.utils import apply_repo_settings
|
||||
from pr_agent.git_providers.git_provider import IncrementalPR
|
||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
||||
from pr_agent.servers.utils import verify_signature
|
||||
from pr_agent.servers.utils import verify_signature, DefaultDictWithTimeout
|
||||
|
||||
setup_logger(fmt=LoggingFormat.JSON)
|
||||
|
||||
@ -47,6 +48,7 @@ async def handle_marketplace_webhooks(request: Request, response: Response):
|
||||
body = await get_body(request)
|
||||
get_logger().info(f'Request body:\n{body}')
|
||||
|
||||
|
||||
async def get_body(request):
|
||||
try:
|
||||
body = await request.json()
|
||||
@ -61,7 +63,9 @@ async def get_body(request):
|
||||
return body
|
||||
|
||||
|
||||
_duplicate_requests_cache = {}
|
||||
_duplicate_requests_cache = DefaultDictWithTimeout(ttl=get_settings().github_app.duplicate_requests_cache_ttl)
|
||||
_duplicate_push_triggers = DefaultDictWithTimeout(ttl=get_settings().github_app.push_trigger_pending_tasks_ttl)
|
||||
_pending_task_duplicate_push_conditions = DefaultDictWithTimeout(asyncio.locks.Condition, ttl=get_settings().github_app.push_trigger_pending_tasks_ttl)
|
||||
|
||||
|
||||
async def handle_request(body: Dict[str, Any], event: str):
|
||||
@ -109,40 +113,111 @@ async def handle_request(body: Dict[str, Any], event: str):
|
||||
# handle pull_request event:
|
||||
# automatically review opened/reopened/ready_for_review PRs as long as they're not in draft,
|
||||
# as well as direct review requests from the bot
|
||||
elif event == 'pull_request':
|
||||
pull_request = body.get("pull_request")
|
||||
if not pull_request:
|
||||
return {}
|
||||
api_url = pull_request.get("url")
|
||||
if not api_url:
|
||||
return {}
|
||||
log_context["api_url"] = api_url
|
||||
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
|
||||
elif event == 'pull_request' and action != 'synchronize':
|
||||
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
|
||||
if not (pull_request and api_url):
|
||||
return {}
|
||||
if action in get_settings().github_app.handle_pr_actions:
|
||||
if action == "review_requested":
|
||||
if body.get("requested_reviewer", {}).get("login", "") != bot_user:
|
||||
return {}
|
||||
if pull_request.get("created_at") == pull_request.get("updated_at"):
|
||||
# avoid double reviews when opening a PR for the first time
|
||||
return {}
|
||||
get_logger().info(f"Performing review because of event={event} and action={action}")
|
||||
apply_repo_settings(api_url)
|
||||
for command in get_settings().github_app.pr_commands:
|
||||
split_command = command.split(" ")
|
||||
command = split_command[0]
|
||||
args = split_command[1:]
|
||||
other_args = update_settings_from_args(args)
|
||||
new_command = ' '.join([command] + other_args)
|
||||
get_logger().info(body)
|
||||
get_logger().info(f"Performing command: {new_command}")
|
||||
with get_logger().contextualize(**log_context):
|
||||
await agent.handle_request(api_url, new_command)
|
||||
get_logger().info(f"Performing review for {api_url=} because of {event=} and {action=}")
|
||||
await _perform_commands(agent, body, api_url, log_context)
|
||||
|
||||
# handle pull_request event with synchronize action - "push trigger" for new commits
|
||||
elif event == 'pull_request' and action == 'synchronize' and get_settings().github_app.handle_push_trigger:
|
||||
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
|
||||
if not (pull_request and api_url):
|
||||
return {}
|
||||
|
||||
# TODO: do we still want to get the list of commits to filter bot/merge commits?
|
||||
before_sha = body.get("before")
|
||||
after_sha = body.get("after")
|
||||
merge_commit_sha = pull_request.get("merge_commit_sha")
|
||||
if before_sha == after_sha:
|
||||
return {}
|
||||
if get_settings().github_app.push_trigger_ignore_merge_commits and after_sha == merge_commit_sha:
|
||||
return {}
|
||||
if get_settings().github_app.push_trigger_ignore_bot_commits and body.get("sender", {}).get("login", "") == bot_user:
|
||||
return {}
|
||||
|
||||
# Prevent triggering multiple times for subsequent push triggers when one is enough:
|
||||
# The first push will trigger the processing, and if there's a second push in the meanwhile it will wait.
|
||||
# Any more events will be discarded, because they will all trigger the exact same processing on the PR.
|
||||
# We let the second event wait instead of discarding it because while the first event was being processed,
|
||||
# more commits may have been pushed that led to the subsequent events,
|
||||
# so we keep just one waiting as a delegate to trigger the processing for the new commits when done waiting.
|
||||
current_active_tasks = _duplicate_push_triggers.setdefault(api_url, 0)
|
||||
max_active_tasks = 2 if get_settings().github_app.push_trigger_pending_tasks_backlog else 1
|
||||
if current_active_tasks < max_active_tasks:
|
||||
# first task can enter, and second tasks too if backlog is enabled
|
||||
get_logger().info(
|
||||
f"Continue processing push trigger for {api_url=} because there are {current_active_tasks} active tasks"
|
||||
)
|
||||
_duplicate_push_triggers[api_url] += 1
|
||||
else:
|
||||
get_logger().info(
|
||||
f"Skipping push trigger for {api_url=} because another event already triggered the same processing"
|
||||
)
|
||||
return {}
|
||||
async with _pending_task_duplicate_push_conditions[api_url]:
|
||||
if current_active_tasks == 1:
|
||||
# second task waits
|
||||
get_logger().info(
|
||||
f"Waiting to process push trigger for {api_url=} because the first task is still in progress"
|
||||
)
|
||||
await _pending_task_duplicate_push_conditions[api_url].wait()
|
||||
get_logger().info(f"Finished waiting to process push trigger for {api_url=} - continue with flow")
|
||||
|
||||
try:
|
||||
if get_settings().github_app.push_trigger_wait_for_initial_review and not get_git_provider()(api_url, incremental=IncrementalPR(True)).previous_review:
|
||||
get_logger().info(f"Skipping incremental review because there was no initial review for {api_url=} yet")
|
||||
return {}
|
||||
get_logger().info(f"Performing incremental review for {api_url=} because of {event=} and {action=}")
|
||||
await _perform_commands(agent, body, api_url, log_context)
|
||||
|
||||
finally:
|
||||
# release the waiting task block
|
||||
async with _pending_task_duplicate_push_conditions[api_url]:
|
||||
_pending_task_duplicate_push_conditions[api_url].notify(1)
|
||||
_duplicate_push_triggers[api_url] -= 1
|
||||
|
||||
get_logger().info("event or action does not require handling")
|
||||
return {}
|
||||
|
||||
|
||||
def _check_pull_request_event(action: str, body: dict, log_context: dict, bot_user: str) -> Tuple[Dict[str, Any], str]:
|
||||
invalid_result = {}, ""
|
||||
pull_request = body.get("pull_request")
|
||||
if not pull_request:
|
||||
return invalid_result
|
||||
api_url = pull_request.get("url")
|
||||
if not api_url:
|
||||
return invalid_result
|
||||
log_context["api_url"] = api_url
|
||||
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
|
||||
return invalid_result
|
||||
if action in ("review_requested", "synchronize") and pull_request.get("created_at") == pull_request.get("updated_at"):
|
||||
# avoid double reviews when opening a PR for the first time
|
||||
return invalid_result
|
||||
return pull_request, api_url
|
||||
|
||||
|
||||
async def _perform_commands(agent: PRAgent, body: dict, api_url: str, log_context: dict):
|
||||
apply_repo_settings(api_url)
|
||||
commands = get_settings().github_app.pr_commands
|
||||
for command in commands:
|
||||
split_command = command.split(" ")
|
||||
command = split_command[0]
|
||||
args = split_command[1:]
|
||||
other_args = update_settings_from_args(args)
|
||||
new_command = ' '.join([command] + other_args)
|
||||
get_logger().info(body)
|
||||
get_logger().info(f"Performing command: {new_command}")
|
||||
with get_logger().contextualize(**log_context):
|
||||
await agent.handle_request(api_url, new_command)
|
||||
|
||||
|
||||
def _is_duplicate_request(body: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
In some deployments its possible to get duplicate requests if the handling is long,
|
||||
@ -150,13 +225,8 @@ def _is_duplicate_request(body: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
request_hash = hash(str(body))
|
||||
get_logger().info(f"request_hash: {request_hash}")
|
||||
request_time = time.monotonic()
|
||||
ttl = get_settings().github_app.duplicate_requests_cache_ttl # in seconds
|
||||
to_delete = [key for key, key_time in _duplicate_requests_cache.items() if request_time - key_time > ttl]
|
||||
for key in to_delete:
|
||||
del _duplicate_requests_cache[key]
|
||||
is_duplicate = request_hash in _duplicate_requests_cache
|
||||
_duplicate_requests_cache[request_hash] = request_time
|
||||
is_duplicate = _duplicate_requests_cache.get(request_hash, False)
|
||||
_duplicate_requests_cache[request_hash] = True
|
||||
if is_duplicate:
|
||||
get_logger().info(f"Ignoring duplicate request {request_hash}")
|
||||
return is_duplicate
|
||||
|
@ -58,13 +58,13 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
||||
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
||||
get_logger().info(f"A merge request has been opened: {data['object_attributes'].get('title')}")
|
||||
url = data['object_attributes'].get('url')
|
||||
handle_request(background_tasks, url, "/review")
|
||||
handle_request(background_tasks, url, "/review", log_context)
|
||||
elif data.get('object_kind') == 'note' and data['event_type'] == 'note':
|
||||
if 'merge_request' in data:
|
||||
mr = data['merge_request']
|
||||
url = mr.get('url')
|
||||
body = data.get('object_attributes', {}).get('note')
|
||||
handle_request(background_tasks, url, body)
|
||||
handle_request(background_tasks, url, body, log_context)
|
||||
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
|
||||
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Callable, Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@ -25,3 +28,59 @@ def verify_signature(payload_body, secret_token, signature_header):
|
||||
class RateLimitExceeded(Exception):
|
||||
"""Raised when the git provider API rate limit has been exceeded."""
|
||||
pass
|
||||
|
||||
|
||||
class DefaultDictWithTimeout(defaultdict):
|
||||
"""A defaultdict with a time-to-live (TTL)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default_factory: Callable[[], Any] = None,
|
||||
ttl: int = None,
|
||||
refresh_interval: int = 60,
|
||||
update_key_time_on_get: bool = True,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
default_factory: The default factory to use for keys that are not in the dictionary.
|
||||
ttl: The time-to-live (TTL) in seconds.
|
||||
refresh_interval: How often to refresh the dict and delete items older than the TTL.
|
||||
update_key_time_on_get: Whether to update the access time of a key also on get (or only when set).
|
||||
"""
|
||||
super().__init__(default_factory, *args, **kwargs)
|
||||
self.__key_times = dict()
|
||||
self.__ttl = ttl
|
||||
self.__refresh_interval = refresh_interval
|
||||
self.__update_key_time_on_get = update_key_time_on_get
|
||||
self.__last_refresh = self.__time() - self.__refresh_interval
|
||||
|
||||
@staticmethod
|
||||
def __time():
|
||||
return time.monotonic()
|
||||
|
||||
def __refresh(self):
|
||||
if self.__ttl is None:
|
||||
return
|
||||
request_time = self.__time()
|
||||
if request_time - self.__last_refresh > self.__refresh_interval:
|
||||
return
|
||||
to_delete = [key for key, key_time in self.__key_times.items() if request_time - key_time > self.__ttl]
|
||||
for key in to_delete:
|
||||
del self[key]
|
||||
self.__last_refresh = request_time
|
||||
|
||||
def __getitem__(self, __key):
|
||||
if self.__update_key_time_on_get:
|
||||
self.__key_times[__key] = self.__time()
|
||||
self.__refresh()
|
||||
return super().__getitem__(__key)
|
||||
|
||||
def __setitem__(self, __key, __value):
|
||||
self.__key_times[__key] = self.__time()
|
||||
return super().__setitem__(__key, __value)
|
||||
|
||||
def __delitem__(self, __key):
|
||||
del self.__key_times[__key]
|
||||
return super().__delitem__(__key)
|
||||
|
@ -34,7 +34,7 @@ key = "" # Optional, uncomment if you want to use Huggingface Inference API. Acq
|
||||
api_base = "" # the base url for your huggingface inference endpoint
|
||||
|
||||
[ollama]
|
||||
api_base = "" # the base url for your huggingface inference endpoint
|
||||
api_base = "" # the base url for your local Llama 2, Code Llama, and other models inference endpoint. Acquire through https://ollama.ai/
|
||||
|
||||
[github]
|
||||
# ---- Set the following only for deployment type == "user"
|
||||
|
@ -24,6 +24,7 @@ num_code_suggestions=4
|
||||
inline_code_comments = false
|
||||
ask_and_reflect=false
|
||||
automatic_review=true
|
||||
remove_previous_review_comment=false
|
||||
extra_instructions = ""
|
||||
|
||||
[pr_description] # /describe #
|
||||
@ -86,6 +87,27 @@ pr_commands = [
|
||||
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
||||
"/auto_review",
|
||||
]
|
||||
# settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits
|
||||
handle_push_trigger = false
|
||||
push_trigger_ignore_bot_commits = true
|
||||
push_trigger_ignore_merge_commits = true
|
||||
push_trigger_wait_for_initial_review = true
|
||||
push_trigger_pending_tasks_backlog = true
|
||||
push_trigger_pending_tasks_ttl = 300
|
||||
push_commands = [
|
||||
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
||||
"""/auto_review -i \
|
||||
--pr_reviewer.require_focused_review=false \
|
||||
--pr_reviewer.require_score_review=false \
|
||||
--pr_reviewer.require_tests_review=false \
|
||||
--pr_reviewer.require_security_review=false \
|
||||
--pr_reviewer.require_estimate_effort_to_review=false \
|
||||
--pr_reviewer.num_code_suggestions=0 \
|
||||
--pr_reviewer.inline_code_comments=false \
|
||||
--pr_reviewer.remove_previous_review_comment=true \
|
||||
--pr_reviewer.extra_instructions='' \
|
||||
"""
|
||||
]
|
||||
|
||||
[gitlab]
|
||||
# URL to the gitlab service
|
||||
|
@ -151,7 +151,7 @@ PR Analysis:
|
||||
PR summary: |-
|
||||
xxx
|
||||
Type of PR: |-
|
||||
Bug fix
|
||||
...
|
||||
{%- if require_score %}
|
||||
Score: 89
|
||||
{%- endif %}
|
||||
|
@ -100,6 +100,9 @@ class PRReviewer:
|
||||
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
||||
get_logger().info(f'Automatic review is disabled {self.pr_url}')
|
||||
return None
|
||||
if self.is_auto and self.incremental.is_incremental and not self.incremental.first_new_commit_sha:
|
||||
get_logger().info(f"Incremental review is enabled for {self.pr_url} but there are no new commits")
|
||||
return None
|
||||
|
||||
get_logger().info(f'Reviewing PR: {self.pr_url} ...')
|
||||
|
||||
@ -113,9 +116,11 @@ class PRReviewer:
|
||||
|
||||
if get_settings().config.publish_output:
|
||||
get_logger().info('Pushing PR review...')
|
||||
previous_review_comment = self._get_previous_review_comment()
|
||||
self.git_provider.publish_comment(pr_comment)
|
||||
self.git_provider.remove_initial_comment()
|
||||
|
||||
if previous_review_comment:
|
||||
self._remove_previous_review_comment(previous_review_comment)
|
||||
if get_settings().pr_reviewer.inline_code_comments:
|
||||
get_logger().info('Pushing inline code comments...')
|
||||
self._publish_inline_code_comments()
|
||||
@ -231,9 +236,14 @@ class PRReviewer:
|
||||
if self.incremental.is_incremental:
|
||||
last_commit_url = f"{self.git_provider.get_pr_url()}/commits/" \
|
||||
f"{self.git_provider.incremental.first_new_commit_sha}"
|
||||
last_commit_msg = self.incremental.commits_range[0].commit.message if self.incremental.commits_range else ""
|
||||
incremental_review_markdown_text = f"Starting from commit {last_commit_url}"
|
||||
if last_commit_msg:
|
||||
replacement = last_commit_msg.splitlines(keepends=False)[0].replace('_', r'\_')
|
||||
incremental_review_markdown_text += f" \n_({replacement})_"
|
||||
data = OrderedDict(data)
|
||||
data.update({'Incremental PR Review': {
|
||||
"⏮️ Review for commits since previous PR-Agent review": f"Starting from commit {last_commit_url}"}})
|
||||
"⏮️ Review for commits since previous PR-Agent review": incremental_review_markdown_text}})
|
||||
data.move_to_end('Incremental PR Review', last=False)
|
||||
|
||||
markdown_text = convert_to_markdown(data, self.git_provider.is_supported("gfm_markdown"))
|
||||
@ -314,3 +324,26 @@ class PRReviewer:
|
||||
break
|
||||
|
||||
return question_str, answer_str
|
||||
|
||||
def _get_previous_review_comment(self):
|
||||
"""
|
||||
Get the previous review comment if it exists.
|
||||
"""
|
||||
try:
|
||||
if get_settings().pr_reviewer.remove_previous_review_comment and hasattr(self.git_provider, "get_previous_review"):
|
||||
return self.git_provider.get_previous_review(
|
||||
full=not self.incremental.is_incremental,
|
||||
incremental=self.incremental.is_incremental,
|
||||
)
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to get previous review comment, error: {e}")
|
||||
|
||||
def _remove_previous_review_comment(self, comment):
|
||||
"""
|
||||
Remove the previous review comment if it exists.
|
||||
"""
|
||||
try:
|
||||
if get_settings().pr_reviewer.remove_previous_review_comment and comment:
|
||||
self.git_provider.remove_comment(comment)
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to remove previous review comment, error: {e}")
|
||||
|
Reference in New Issue
Block a user