mirror of
https://github.com/hustcer/deepseek-review.git
synced 2026-05-13 05:16:05 +08:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9163ca2434 | ||
|
|
cb78a000a7 | ||
|
|
d1dcf1311d | ||
|
|
6cbc5ef631 | ||
|
|
92a3c2ae17 | ||
|
|
90c8878923 | ||
|
|
231c2d8432 | ||
|
|
923ee0f999 | ||
|
|
32cffba4d1 | ||
|
|
0c001b0ee2 | ||
|
|
edcd90e48b | ||
|
|
dfc4c59c1f | ||
|
|
92061e6c82 | ||
|
|
3c3f0a3c85 | ||
|
|
7a87f8adc0 | ||
|
|
d7b2e3a926 | ||
|
|
91ae07bb82 | ||
|
|
d830da6937 | ||
|
|
885b55dbb0 | ||
|
|
317d3c92da | ||
|
|
938790c65b | ||
|
|
a4d125ecba | ||
|
|
930e0ae68f | ||
|
|
d0b2ea125a | ||
|
|
9852113028 | ||
|
|
9ad3373cd5 | ||
|
|
981ef03409 | ||
|
|
9852118ba7 | ||
|
|
985f205ae5 | ||
|
|
29aec71797 | ||
|
|
957db0afb6 | ||
|
|
c53edfe925 | ||
|
|
0298773233 | ||
|
|
9d6bb02502 | ||
|
|
26e38f1543 | ||
|
|
8b7262a6b4 | ||
|
|
0679ef2b0e | ||
|
|
ac1bb26376 | ||
|
|
443d1d887e | ||
|
|
90d7be5ff2 | ||
|
|
9af6f3d480 | ||
|
|
abe4d66650 | ||
|
|
da010fada9 | ||
|
|
61a7b5f654 |
4
.github/workflows/cr.yml
vendored
4
.github/workflows/cr.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
As a senior Nushell engineer, perform comprehensive script review with focus on:
|
||||
|
||||
### 1. Core Requirements:
|
||||
- Validate Nu 0.90+ compatibility
|
||||
- Validate Nu 0.100+ compatibility
|
||||
- Check structured data handling
|
||||
- Verify pipeline efficiency
|
||||
- Assess module organization
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- Parallel execution opportunities
|
||||
|
||||
**Rules:**
|
||||
- Target Nu 0.90+ features
|
||||
- Target Nu 0.100+ features
|
||||
- Highlight data flow vulnerabilities
|
||||
- Suggest structured data optimizations
|
||||
- Keep feedback Nu-specific
|
||||
|
||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -15,6 +15,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- feature/test
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
@@ -41,12 +42,12 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout Nutest Repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: v1.0.1
|
||||
ref: main
|
||||
path: nutest
|
||||
repository: vyadh/nutest
|
||||
sparse-checkout: nutest/
|
||||
@@ -62,7 +63,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
use ${{ github.workspace }}/nutest/nutest
|
||||
use ${{ github.workspace }}/nu/review.nu [prepare-awk]
|
||||
use ${{ github.workspace }}/nu/util.nu [prepare-awk]
|
||||
prepare-awk
|
||||
(
|
||||
nutest run-tests
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.env
|
||||
.env.dev
|
||||
.env.local
|
||||
review.md
|
||||
config.yml
|
||||
prompts.yaml
|
||||
|
||||
89
CHANGELOG.md
89
CHANGELOG.md
@@ -1,6 +1,95 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [1.20.0] - 2026-01-23
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Make config-check use default value for --config flag
|
||||
- Fix `from env` for .env file parsing (#193)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update minimum required Nushell version to 0.110.0
|
||||
- Update nutest to main ref (#191)
|
||||
- Update README.md
|
||||
|
||||
### Refactor
|
||||
|
||||
- A better `from env` parser (#194)
|
||||
|
||||
### Deps
|
||||
|
||||
- Upgrade to actions/checkout@v5
|
||||
- Upgrade `hustcer/setup-nu` to v3.20
|
||||
- Upgrade Nushell version to 0.108.0 (#190)
|
||||
- Upgrade `hustcer/setup-nu` to v3.21 (#192)
|
||||
- Upgrade actions/checkout@v6
|
||||
- Upgrade Nu to 0.109.1 (#195)
|
||||
- Update Nushell to 0.110.0 (#196)
|
||||
- Upgrade `hustcer/setup-nu` to v3.22
|
||||
|
||||
## [1.19.0] - 2025-07-23
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix "variable not found" error (#185)
|
||||
- Fix getting Nu binary path for Nushell 0.106
|
||||
|
||||
### Deps
|
||||
|
||||
- Upgrade Nu to v0.106 (#186)
|
||||
|
||||
## [1.18.0] - 2025-06-11
|
||||
|
||||
### Features
|
||||
|
||||
- Set default `temperature` to **0.3** for code review (#181)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Refine diff flag descriptions in docs and scripts (#177)
|
||||
- Upgrade `Nu` to 0.105 and pin [`hustcer/setup-nu`](https://github.com/hustcer/setup-nu) to v3.19 (#183)
|
||||
|
||||
### Deps
|
||||
|
||||
- Upgrade `nutest` to v1.1.0 (#179)
|
||||
|
||||
## [1.17.0] - 2025-04-11
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Read default `include` and `exclude` patterns from config for local code review (#170)
|
||||
|
||||
### Features
|
||||
|
||||
All the following changes are for local code review only:
|
||||
|
||||
- Add code review for `git show head:path/to/file` patch command support (#171)
|
||||
- Add write code review result to file by `--output` flag support (#172)
|
||||
|
||||
## [1.16.0] - 2025-04-05
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add alias setup guide for `powershell` (#163)
|
||||
|
||||
### Features
|
||||
|
||||
- Add OpenRouter deepseek model support (#167)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Add alias setup guide for `fish`
|
||||
- Add openrouter.ai config example
|
||||
- Set minimum required `nushell` version to v0.103
|
||||
|
||||
### Refactor
|
||||
|
||||
- Refactor `get-diff` custom command (#164)
|
||||
- Refactor diff handling by moving logic to separate module (#165)
|
||||
- Replace custom `kv.nu` module with `std-rfc/kv` for key-value functionality (#166)
|
||||
|
||||
## [1.15.0] - 2025-03-23
|
||||
|
||||
### Features
|
||||
|
||||
2
Justfile
2
Justfile
@@ -28,7 +28,7 @@ alias cr := code-review
|
||||
|
||||
# Used to handle the path separator issue
|
||||
DEEPSEEK_REVIEW_PATH := parent_directory(justfile())
|
||||
NU_DIR := parent_directory(`(which nu).path.0`)
|
||||
NU_DIR := parent_directory(`$nu.current-exe`)
|
||||
_query_plugin := if os_family() == 'windows' { 'nu_plugin_query.exe' } else { 'nu_plugin_query' }
|
||||
|
||||
# To pass arguments to a dependency, put the dependency
|
||||
|
||||
103
README.md
103
README.md
@@ -7,30 +7,29 @@
|
||||
|
||||
[中文说明](README.zh-CN.md)
|
||||
|
||||
`deepseek-review` also offers seamless integration with DeepSeek models on SiliconCloud. [Sign Up Now](https://cloud.siliconflow.cn/i/rqCdIxzS) to **Claim Your Free 20 Million Tokens** and start exploring its capabilities!
|
||||
|
||||
## Features
|
||||
|
||||
### GitHub Action
|
||||
|
||||
- Automate PR Reviews with DeepSeek via GitHub Action
|
||||
- Add `skip cr` or `skip review` to PR Title or Body to Disable Code Review in GitHub Actions
|
||||
- Add `skip cr` or `skip review` to the PR title or body to disable code review in GitHub Actions
|
||||
- Cross-platform Support: Compatible with GitHub Runners across `macOS`, `Ubuntu`, and `Windows`.
|
||||
|
||||
### Local Code Review
|
||||
|
||||
- Streaming Output Support for Local Code Review
|
||||
- Review Remote GitHub PRs Directly from Your Local CLI
|
||||
- Review Commit Changes with DeepSeek for Any Local Repository by CLI
|
||||
- Support On-demand Changes Generation via `git show`/`git diff` Command for Further Code Review
|
||||
- Cross-platform Compatibility: Designed to function seamlessly across all platforms capable of running [Nushell](https://github.com/nushell/nushell)
|
||||
- Streaming output support for local code review
|
||||
- Review remote GitHub PRs directly from your local CLI
|
||||
- Review commit changes with DeepSeek for any local repository via CLI
|
||||
- Support on-demand change generation via `git show`/`git diff` commands for further code review
|
||||
- Output code review results to a specified file in Markdown format
|
||||
- Cross-platform compatibility: Designed to function seamlessly across all platforms capable of running [Nushell](https://github.com/nushell/nushell)
|
||||
|
||||
### Both GH Action & Local
|
||||
|
||||
- Support Both DeepSeek's `V3` & `R1` Models
|
||||
- Fully Customizable: Choose Models, Base URLs, and Prompts
|
||||
- Supports Self-Hosted DeepSeek Models for Enhanced Flexibility
|
||||
- Perform Code Reviews for Changes That either Include or Exclude Specific Files
|
||||
- Support both DeepSeek's `V3` and `R1` models
|
||||
- Fully customizable: Choose models, base URLs, and prompts
|
||||
- Support self-hosted DeepSeek models for enhanced flexibility
|
||||
- Perform code reviews for changes that either include or exclude specific files
|
||||
|
||||
## Planned Features
|
||||
|
||||
@@ -39,7 +38,7 @@
|
||||
|
||||
## Code Review with GitHub Action
|
||||
|
||||
### Initiate Code Review When PR was Created
|
||||
### Initiate Code Review When a PR is Created
|
||||
|
||||
Add a GitHub workflow with the following contents:
|
||||
|
||||
@@ -81,13 +80,13 @@ jobs:
|
||||
|
||||
</details>
|
||||
|
||||
When a PR is created, DeepSeek code review will be automatically triggered, and the review results(depend on your prompt) will be posted as comments on the corresponding PR. For example:
|
||||
When a PR is created, DeepSeek code review will be automatically triggered, and the review results (depending on your prompt) will be posted as comments on the corresponding PR. For example:
|
||||
- [Example 1](https://github.com/hustcer/deepseek-review/pull/30) with [default prompts](https://github.com/hustcer/deepseek-review/blob/main/action.yaml#L35) & [Run Log](https://github.com/hustcer/deepseek-review/actions/runs/13043609677/job/36390331791#step:2:53).
|
||||
- [Example 2](https://github.com/hustcer/deepseek-review/pull/68) with [this prompt](https://github.com/hustcer/deepseek-review/blob/eba892d969049caff00b51a31e5c093aeeb536e3/.github/workflows/cr.yml#L32)
|
||||
|
||||
### Trigger CR When a Specific Label was Added
|
||||
### Trigger Code Review When a Specific Label is Added
|
||||
|
||||
If you don't want automatic review on PR creation, you can choose to trigger code review by adding a label. For example, create the following workflow:
|
||||
If you don't want automatic code review on PR creation, you can choose to trigger code review by adding a label. For example, create the following workflow:
|
||||
|
||||
```yaml
|
||||
name: Code Review
|
||||
@@ -120,21 +119,21 @@ With this setup, DeepSeek code review will not run automatically upon PR creatio
|
||||
| Name | Type | Description |
|
||||
| -------------- | ------ | ----------------------------------------------------------------------- |
|
||||
| chat-token | String | Required, DeepSeek API Token |
|
||||
| model | String | Optional, The model used for code review, defaults to `deepseek-chat` |
|
||||
| model | String | Optional, The model used for code review, defaults to `deepseek-v4-flash` |
|
||||
| base-url | String | Optional, DeepSeek API Base URL, defaults to `https://api.deepseek.com` |
|
||||
| max-length | Int | Optional, Maximum length(Unicode width) of the content for review, if the content length exceeds this value, the review will be skipped. Default `0` means no limit. |
|
||||
| max-length | Int | Optional, Maximum length (Unicode width) of the content for review. If the content length exceeds this value, the review will be skipped. Default `0` means no limit. |
|
||||
| sys-prompt | String | Optional, System prompt corresponding to `$sys_prompt` in the payload, default value see note below |
|
||||
| user-prompt | String | Optional, User prompt corresponding to `$user_prompt` in the payload, default value see note below |
|
||||
| temperature | Number | Optional, The temperature for the model to generate the response, between `0` and `2`, default value `1.0` |
|
||||
| include-patterns | String | Optional, The comma separated file patterns to include in the code review. No default |
|
||||
| exclude-patterns | String | Optional, The comma separated file patterns to exclude in the code review. Default to `pnpm-lock.yaml,package-lock.json,*.lock` |
|
||||
| temperature | Number | Optional, The temperature for the model to generate the response, between `0` and `2`. Default value is `0.3` |
|
||||
| include-patterns | String | Optional, Comma-separated file patterns to include in the code review. No default |
|
||||
| exclude-patterns | String | Optional, Comma-separated file patterns to exclude from the code review. Defaults to `pnpm-lock.yaml,package-lock.json,*.lock` |
|
||||
| github-token | String | Optional, The `GITHUB_TOKEN` secret or personal access token to authenticate. Defaults to `${{ github.token }}`. |
|
||||
|
||||
**DeepSeek API Call Payload**:
|
||||
|
||||
```js
|
||||
{
|
||||
// `$model` default value: deepseek-chat
|
||||
// `$model` default value: deepseek-v4-flash
|
||||
model: $model,
|
||||
stream: false,
|
||||
temperature: $temperature,
|
||||
@@ -153,18 +152,18 @@ With this setup, DeepSeek code review will not run automatically upon PR creatio
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> You can control the language of the code review results by the language of the
|
||||
> Prompt. The default Prompt language is currently English. When you use a Chinese
|
||||
> Prompt, the generated code review results will be in Chinese.
|
||||
> You can control the language of the code review results through the language of the
|
||||
> prompt. The default prompt language is currently English. When you use a Chinese
|
||||
> prompt, the generated code review results will be in Chinese.
|
||||
|
||||
## Local Code Review
|
||||
|
||||
### Required Tools
|
||||
|
||||
To perform code reviews locally(should works for `macOS`, `Ubuntu`, and `Windows`), you need to install the following tools:
|
||||
To perform code reviews locally (works on `macOS`, `Ubuntu`, and `Windows`), you need to install the following tools:
|
||||
|
||||
- [`Nushell`](https://www.nushell.sh/book/installation.html). It is recommended to install the latest versions.
|
||||
- The latest version of [`awk`](https://github.com/onetrueawk/awk) or [`gawk`](https://www.gnu.org/software/gawk/) is required, with `gawk` being the preferred choice.
|
||||
- [`Nushell`](https://www.nushell.sh/book/installation.html). It is recommended to install the latest version (minimum version required: `0.112.2`).
|
||||
- The latest version of [`awk`](https://github.com/onetrueawk/awk) or [`gawk`](https://www.gnu.org/software/gawk/) is required, with `gawk` being preferred.
|
||||
- Clone this repository to your local machine, navigate to the repository directory, and run `nu cr -h`. You should see an output similar to the following:
|
||||
|
||||
```console
|
||||
@@ -178,19 +177,20 @@ Flags:
|
||||
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review
|
||||
-n, --pr-number <string>: GitHub PR number
|
||||
-k, --gh-token <string>: Your GitHub token, fallback to GITHUB_TOKEN env var
|
||||
-t, --diff-to <string>: Diff to git REF
|
||||
-f, --diff-from <string>: Diff from git REF
|
||||
-f, --diff-from <string>: Git diff starting commit SHA
|
||||
-t, --diff-to <string>: Git diff ending commit SHA
|
||||
-c, --patch-cmd <string>: The `git show` or `git diff` command to get the diff content, for local CR only
|
||||
-l, --max-length <int>: Maximum length of the content for review, 0 means no limit.
|
||||
-m, --model <string>: Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
|
||||
-m, --model <string>: Model name, or read from CHAT_MODEL env var, `deepseek-v4-flash` by default
|
||||
-b, --base-url <string>: DeepSeek API base URL, fallback to BASE_URL env var
|
||||
-U, --chat-url <string>: DeepSeek Model chat full API URL, e.g. http://localhost:11535/api/chat
|
||||
-s, --sys-prompt <string>: Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||
-i, --include <string>: Comma separated file patterns to include in the code review
|
||||
-x, --exclude <string>: Comma separated file patterns to exclude in the code review
|
||||
-T, --temperature <float>: Temperature for the model, between `0` and `2`, default value `1.0`
|
||||
-T, --temperature <float>: Temperature for the model, between `0` and `2`, default value `0.3`
|
||||
-C, --config <string>: Config file path, default to `config.yml`
|
||||
-o, --output <string>: Output file path
|
||||
-h, --help: Display the help message for this command
|
||||
|
||||
Parameters:
|
||||
@@ -200,25 +200,32 @@ Parameters:
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
To perform code review locally, you need to modify the configuration file. The repository already provides a configuration example [`config.example.yml`](https://github.com/hustcer/deepseek-review/blob/main/config.example.yml). Copy it to `config.yml` and modify it according to your actual needs. **Read the comments in the configuration file carefully**, as they explain the purpose of each configuration item.
|
||||
To perform code reviews locally, you need to modify the configuration file. The repository provides a configuration example [`config.example.yml`](https://github.com/hustcer/deepseek-review/blob/main/config.example.yml). Copy it to `config.yml` and modify it according to your needs. **Read the comments in the configuration file carefully**, as they explain the purpose of each configuration item.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> The `config.yml` configuration file is **only used locally** and will not be utilized in GitHub Workflow. **Sensitive information** in this file should be properly secured and **never committed** to the code repository.
|
||||
> The `config.yml` configuration file is **only used locally** and will not be utilized in GitHub Workflows. **Sensitive information** in this file should be properly secured and **never committed** to the code repository.
|
||||
>
|
||||
|
||||
**Create Command Alias**
|
||||
**Create a Command Alias**
|
||||
|
||||
For convenience in performing code review across any local repository, create a command alias. For example:
|
||||
For convenience when performing code reviews across any local repository, create a command alias. For example:
|
||||
|
||||
```sh
|
||||
# For Nushell: Modify config.nu and add:
|
||||
alias cr = nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml
|
||||
|
||||
# For zsh/bash: Modify ~/.zshrc or ~/.bashrc and add:
|
||||
# Modify ~/.zshrc for zsh, ~/.bashrc for bash, or ~/.config/fish/config.fish for fish and add:
|
||||
alias cr="nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml"
|
||||
|
||||
# After sourcing the modified profile, use `cr` for code review
|
||||
# After sourcing the modified profile, use `cr` for code reviews
|
||||
|
||||
# For Windows PowerShell users, set the cr alias by editing $PROFILE and add:
|
||||
function cr {
|
||||
nu D:\absolute\path\to\deepseek-review\cr --config D:\absolute\path\to\deepseek-review\config.yml @args
|
||||
}
|
||||
|
||||
# Then restart the terminal or run `. $PROFILE` in pwsh to make `cr` work
|
||||
```
|
||||
|
||||
### Review Local Repository
|
||||
@@ -226,18 +233,20 @@ alias cr="nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/dee
|
||||
To review a local repository:
|
||||
|
||||
- Navigate to the Git repository directory.
|
||||
- Use the `cr` command to review current modifications, provided that `config.yml` is correctly configured.
|
||||
- Use the `cr` command to review current modifications, provided that `config.yml` is correctly configured.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
```sh
|
||||
# Perform code review on the `git diff` changes in current directory
|
||||
# Perform code review on the `git diff` changes in the current directory
|
||||
cr
|
||||
# Perform code review on the `git diff f536acc` changes in current directory
|
||||
# Perform code review on the `git diff f536acc` changes in the current directory
|
||||
cr --diff-from f536acc
|
||||
# Perform code review on the `git diff f536acc 0dd0eb5` changes in current directory
|
||||
# Perform code review on the `git diff f536acc` changes and output the result to review.md
|
||||
cr --diff-from f536acc --output review.md
|
||||
# Perform code review on the `git diff f536acc 0dd0eb5` changes in the current directory
|
||||
cr --diff-from f536acc --diff-to 0dd0eb5
|
||||
# Review the changes in current directory using the `--patch-cmd` flag
|
||||
# Review the changes in the current directory using the `--patch-cmd` flag
|
||||
cr --patch-cmd 'git diff head~3'
|
||||
cr -c 'git show head~3'
|
||||
cr -c 'git diff 2393375 71f5a31'
|
||||
@@ -250,17 +259,17 @@ cr -c 'git diff 2393375 71f5a31 :!nu/*'
|
||||
|
||||
When reviewing a remote GitHub PR locally:
|
||||
|
||||
- Always specify the PR number via `--pr-number`
|
||||
- Always specify the PR number via `--pr-number`.
|
||||
- Use `--repo` to indicate the target repository (e.g., `hustcer/deepseek-review`). If `--repo` is omitted, the tool reads `settings.default-github-repo` from `config.yml`.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
```sh
|
||||
# Perform code review on PR #31 in the remote DEFAULT_GITHUB_REPO repo
|
||||
# Perform code review on PR #31 in the remote DEFAULT_GITHUB_REPO repository
|
||||
cr --pr-number 31
|
||||
# Perform code review on PR #31 in the remote hustcer/deepseek-review repo
|
||||
# Perform code review on PR #31 in the remote hustcer/deepseek-review repository
|
||||
cr --pr-number 31 --repo hustcer/deepseek-review
|
||||
# Perform code review on PR #31 and exclude changes of pnpm-lock.yaml
|
||||
# Perform code review on PR #31 and exclude changes in pnpm-lock.yaml
|
||||
cr --pr-number 31 --exclude pnpm-lock.yaml
|
||||
```
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||

|
||||

|
||||
|
||||
本工具也支持使用 SiliconCloud 上的 DeepSeek 模型,[注册](https://cloud.siliconflow.cn/i/rqCdIxzS) 就**免费赠送 2000 万 Token**,赶紧试试吧!
|
||||
|
||||
## 特性
|
||||
|
||||
### GitHub Action
|
||||
@@ -21,6 +19,7 @@
|
||||
- 通过本地 CLI 直接审查远程 GitHub PR
|
||||
- 通过本地 CLI 使用 DeepSeek 审查任何本地仓库的提交变更
|
||||
- 允许通过自定义 `git show`/`git diff` 命令生成变更记录并进行审查
|
||||
- 允许将代码审查结果以 Markdown 格式输出到指定文件
|
||||
- 跨平台:理论上只要能运行 [Nushell](https://github.com/nushell/nushell) 即可使用本工具
|
||||
|
||||
### 本地或 GH Action
|
||||
@@ -118,12 +117,12 @@ jobs:
|
||||
| 名称 | 类型 | 描述 |
|
||||
| -------------- | ------ | -------------------------------------------------------------- |
|
||||
| chat-token | String | 必填,DeepSeek API Token |
|
||||
| model | String | 可选,配置代码审查选用的模型,默认为 `deepseek-chat` |
|
||||
| model | String | 可选,配置代码审查选用的模型,默认为 `deepseek-v4-flash` |
|
||||
| base-url | String | 可选,DeepSeek API Base URL, 默认为 `https://api.deepseek.com` |
|
||||
| max-length | Int | 可选,待审查内容的最大 Unicode 长度, 默认 `0` 表示没有限制,超过非零值则跳过审查 |
|
||||
| sys-prompt | String | 可选,系统提示词对应入参中的 `$sys_prompt`, 默认值见后文注释 |
|
||||
| user-prompt | String | 可选,用户提示词对应入参中的 `$user_prompt`, 默认值见后文注释 |
|
||||
| temperature | Number | 可选,采样温度,介于 `0` 和 `2` 之间, 默认值 `1.0` |
|
||||
| temperature | Number | 可选,采样温度,介于 `0` 和 `2` 之间, 默认值 `0.3` |
|
||||
| include-patterns | String | 可选,代码审查中要包含的以逗号分隔的文件模式,无默认值 |
|
||||
| exclude-patterns | String | 可选,代码审查中要排除的以逗号分隔的文件模式,默认值为 `pnpm-lock.yaml,package-lock.json,*.lock` |
|
||||
| github-token | String | 可选,用于访问 API 进行 PR 管理的 GitHub Token,默认为 `${{ github.token }}` |
|
||||
@@ -132,7 +131,7 @@ DeepSeek 接口调用入参:
|
||||
|
||||
```js
|
||||
{
|
||||
// `$model` default value: deepseek-chat
|
||||
// `$model` default value: deepseek-v4-flash
|
||||
model: $model,
|
||||
stream: false,
|
||||
temperature: $temperature,
|
||||
@@ -160,7 +159,7 @@ DeepSeek 接口调用入参:
|
||||
|
||||
在本地进行代码审查,支持 `macOS`, `Ubuntu` & `Windows` 不过需要安装以下工具:
|
||||
|
||||
- [`Nushell`](https://www.nushell.sh/book/installation.html), 建议安装最新版本
|
||||
- [`Nushell`](https://www.nushell.sh/book/installation.html), 建议安装最新版本(最低版本 `0.112.2`)
|
||||
- [`awk`](https://github.com/onetrueawk/awk) 或者 [`gawk`](https://www.gnu.org/software/gawk/) 的最新版版本,优先推荐 `gawk`
|
||||
- 接下来只需要把本仓库代码克隆到本地,然后进入仓库目录执行 `nu cr -h` 即可看到类似如下输出:
|
||||
|
||||
@@ -175,19 +174,20 @@ Flags:
|
||||
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review
|
||||
-n, --pr-number <string>: GitHub PR number
|
||||
-k, --gh-token <string>: Your GitHub token, fallback to GITHUB_TOKEN env var
|
||||
-t, --diff-to <string>: Diff to git REF
|
||||
-f, --diff-from <string>: Diff from git REF
|
||||
-f, --diff-from <string>: Git diff starting commit SHA
|
||||
-t, --diff-to <string>: Git diff ending commit SHA
|
||||
-c, --patch-cmd <string>: The `git show` or `git diff` command to get the diff content, for local CR only
|
||||
-l, --max-length <int>: Maximum length of the content for review, 0 means no limit.
|
||||
-m, --model <string>: Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
|
||||
-m, --model <string>: Model name, or read from CHAT_MODEL env var, `deepseek-v4-flash` by default
|
||||
-b, --base-url <string>: DeepSeek API base URL, fallback to BASE_URL env var
|
||||
-U, --chat-url <string>: DeepSeek Model chat full API URL, e.g. http://localhost:11535/api/chat
|
||||
-s, --sys-prompt <string>: Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||
-i, --include <string>: Comma separated file patterns to include in the code review
|
||||
-x, --exclude <string>: Comma separated file patterns to exclude in the code review
|
||||
-T, --temperature <float>: Temperature for the model, between `0` and `2`, default value `1.0`
|
||||
-T, --temperature <float>: Temperature for the model, between `0` and `2`, default value `0.3`
|
||||
-C, --config <string>: Config file path, default to `config.yml`
|
||||
-o, --output <string>: Output file path
|
||||
-h, --help: Display the help message for this command
|
||||
|
||||
Parameters:
|
||||
@@ -212,9 +212,17 @@ Parameters:
|
||||
```sh
|
||||
# Nushell: 修改其 config.nu 配置文件,添加:
|
||||
alias cr = nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml
|
||||
# 对于 zsh 或 bash分别修改 ~/.zshrc or ~/.bashrc and add:
|
||||
|
||||
# Modify ~/.zshrc for zsh or ~/.bashrc for bash or ~/.config/fish/config.fish for fish and add:
|
||||
alias cr="nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml"
|
||||
# After sourcing the profile you have edit, you can use `cr` now
|
||||
# After sourcing the profile you have edit, you can use `cr` now
|
||||
|
||||
# For Windows powershell users please set cr alias by editing $PROFILE and add:
|
||||
function cr {
|
||||
nu D:\absolute\path\to\deepseek-review\cr --config D:\absolute\path\to\deepseek-review\config.yml @args
|
||||
}
|
||||
|
||||
# Then restart the terminal or run `. $PROFILE` in pwsh to make `cr` work
|
||||
```
|
||||
|
||||
之后就可以通过 `cr` 命令来进行代码审查了。
|
||||
@@ -230,6 +238,8 @@ alias cr="nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/dee
|
||||
cr
|
||||
# 对本地当前目录所在仓库 `git diff f536acc` 修改内容进行代码审查
|
||||
cr --diff-from f536acc
|
||||
# 对本地当前目录所在仓库 `git diff f536acc` 修改内容进行代码审查并将审查结果输出到 review.md
|
||||
cr --diff-from f536acc --output review.md
|
||||
# 对本地当前目录所在仓库 `git diff f536acc 0dd0eb5` 修改内容进行代码审查
|
||||
cr --diff-from f536acc --diff-to 0dd0eb5
|
||||
# 通过 --patch-cmd 参数对本地当前目录所在仓库变更内容进行审查
|
||||
|
||||
10
action.yaml
10
action.yaml
@@ -24,11 +24,11 @@ inputs:
|
||||
description: 'The maximum length of the content for review, 0 means no limit.'
|
||||
model:
|
||||
required: false
|
||||
default: 'deepseek-chat'
|
||||
default: 'deepseek-v4-flash'
|
||||
description: 'The DeepSeek model to choose for code review.'
|
||||
temperature:
|
||||
required: false
|
||||
default: 1.0
|
||||
default: 0.3
|
||||
description: 'The temperature of the model.'
|
||||
base-url:
|
||||
required: false
|
||||
@@ -58,9 +58,9 @@ runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Nu
|
||||
uses: hustcer/setup-nu@v3
|
||||
uses: hustcer/setup-nu@v3.23
|
||||
with:
|
||||
version: 0.103.0
|
||||
version: 0.112.2
|
||||
|
||||
- name: DeepSeek Code Review
|
||||
shell: nu {0}
|
||||
@@ -78,7 +78,7 @@ runs:
|
||||
let includePatterns = '${{ inputs.include-patterns }}'
|
||||
let excludePatterns = '${{ inputs.exclude-patterns }}'
|
||||
let maxLength = try { '${{ inputs.max-length }}' | into int } catch { 0 }
|
||||
let temperature = try { '${{ inputs.temperature }}' | into float } catch { 1.0 }
|
||||
let temperature = try { '${{ inputs.temperature }}' | into float } catch { 0.3 }
|
||||
(deepseek-review $token
|
||||
--model $model
|
||||
--repo $repo
|
||||
|
||||
@@ -17,8 +17,8 @@ settings:
|
||||
# If the content length exceeds the non-zero limit, the review will be skipped
|
||||
# Note that it's unicode width not LLM token length
|
||||
max-length: 0
|
||||
# The temperature of the model, The value should be between 0 and 2, with default value 1.0
|
||||
temperature: 1.0
|
||||
# The temperature of the model, The value should be between 0 and 2, with default value 0.3
|
||||
temperature: 0.3
|
||||
# The user prompt name to use for DeepSeek API select from 'prompts.user'
|
||||
user-prompt: 'default'
|
||||
# The system prompt name to use for DeepSeek API select from 'prompts.system'
|
||||
@@ -45,11 +45,12 @@ providers:
|
||||
alias: r1
|
||||
enabled: true
|
||||
description: 'DeepSeek R1 model running on Ollama'
|
||||
|
||||
- name: 'DeepSeek'
|
||||
token: 'YOUR_DEEPSEEK_TOKEN' # Required, The API token for the provider
|
||||
base-url: 'https://api.deepseek.com'
|
||||
models:
|
||||
- name: 'deepseek-chat' # Required, Pass the model name to --model flag to use it
|
||||
- name: 'deepseek-v4-flash' # Required, Pass the model name to --model flag to use it
|
||||
alias: v3 # Optional, Alias name could also be passed to --model flag
|
||||
enabled: true # One and Only one model could be enabled in one model group
|
||||
description: 'DeepSeek V3' # Optional, Description of the model, won't be used actually
|
||||
@@ -70,6 +71,18 @@ providers:
|
||||
alias: r1
|
||||
description: 'SiliconFlow DeepSeek R1 model'
|
||||
|
||||
- name: OpenRouter
|
||||
token: sk-or-v1-*****
|
||||
base-url: https://openrouter.ai/api/v1
|
||||
models:
|
||||
- name: deepseek/deepseek-chat-v3-0324:free
|
||||
alias: v3
|
||||
enabled: true
|
||||
description: 'OpenRouter DeepSeek V3 model'
|
||||
- name: deepseek/deepseek-r1:free
|
||||
alias: r1
|
||||
description: 'OpenRouter DeepSeek R1 model'
|
||||
|
||||
# Multiple Prompts could be defined, select the one by name in 'settings.user-prompt' or 'settings.system-prompt'
|
||||
prompts:
|
||||
user:
|
||||
|
||||
16
cr
16
cr
@@ -4,8 +4,8 @@
|
||||
# Description: A wrapper for nu/review.nu as the main entry point of the project.
|
||||
|
||||
use nu/config.nu *
|
||||
use nu/common.nu [hr-line, check-nushell, ECODE]
|
||||
use nu/review.nu [deepseek-review]
|
||||
use nu/common.nu [hr-line, check-nushell, ECODE]
|
||||
|
||||
# Use DeepSeek AI to review code changes locally or in GitHub Actions
|
||||
def main [
|
||||
@@ -14,19 +14,20 @@ def main [
|
||||
--repo(-r): string, # GitHub repo name, e.g. hustcer/deepseek-review
|
||||
--pr-number(-n): string, # GitHub PR number
|
||||
--gh-token(-k): string, # Your GitHub token, fallback to GITHUB_TOKEN env var
|
||||
--diff-to(-t): string, # Diff to git REF
|
||||
--diff-from(-f): string, # Diff from git REF
|
||||
--diff-from(-f): string, # Git diff starting commit SHA
|
||||
--diff-to(-t): string, # Git diff ending commit SHA
|
||||
--patch-cmd(-c): string, # The `git show` or `git diff` command to get the diff content, for local CR only
|
||||
--max-length(-l): int, # Maximum length of the content for review, 0 means no limit.
|
||||
--model(-m): string, # Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
|
||||
--model(-m): string, # Model name, or read from CHAT_MODEL env var, `deepseek-v4-flash` by default
|
||||
--base-url(-b): string, # DeepSeek API base URL, fallback to BASE_URL env var
|
||||
--chat-url(-U): string, # DeepSeek Model chat full API URL, e.g. http://localhost:11535/api/chat
|
||||
--sys-prompt(-s): string # Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||
--include(-i): string, # Comma separated file patterns to include in the code review
|
||||
--exclude(-x): string, # Comma separated file patterns to exclude in the code review
|
||||
--temperature(-T): float, # Temperature for the model, between `0` and `2`, default value `1.0`
|
||||
--temperature(-T): float, # Temperature for the model, between `0` and `2`, default value `0.3`
|
||||
--config(-C): string # Config file path, default to `config.yml`
|
||||
--output(-o): string, # Output file path
|
||||
] {
|
||||
|
||||
check-nushell
|
||||
@@ -36,8 +37,7 @@ def main [
|
||||
deepseek-review $token
|
||||
--repo=$repo
|
||||
--debug=$debug
|
||||
--include=$include
|
||||
--exclude=$exclude
|
||||
--output=$output
|
||||
--model=$env.CHAT_MODEL
|
||||
--base-url=$base_url
|
||||
--chat-url=$chat_url
|
||||
@@ -50,5 +50,7 @@ def main [
|
||||
--sys-prompt=$sys_prompt
|
||||
--user-prompt=$user_prompt
|
||||
--temperature=$temperature
|
||||
--include=($include | default $env.INCLUDE_PATTERNS?)
|
||||
--exclude=($exclude | default $env.EXCLUDE_PATTERNS?)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,16 +20,18 @@ words:
|
||||
- hustcer
|
||||
- Nushell
|
||||
- creatio
|
||||
- pipefail
|
||||
- justfile
|
||||
- lefthook
|
||||
- deepseek
|
||||
- linewise
|
||||
- Subshell
|
||||
- subshells
|
||||
- noreferrer
|
||||
- OPENROUTER
|
||||
- Infinigence
|
||||
- SILICONFLOW
|
||||
- USERPROFILE
|
||||
- noreferrer
|
||||
- Unsanitized
|
||||
- Unvalidated
|
||||
- monomorphization
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "deepseek-review",
|
||||
"version": "1.15.0",
|
||||
"actionVer": "v1.15",
|
||||
"version": "1.20.0",
|
||||
"actionVer": "v1.20",
|
||||
"author": "hustcer",
|
||||
"license": "MIT",
|
||||
"github": "https://github.com/hustcer/deepseek-review",
|
||||
|
||||
94
nu/common.nu
94
nu/common.nu
@@ -4,7 +4,7 @@
|
||||
# Description: Common helpers for DeepSeek-Review
|
||||
#
|
||||
|
||||
use kv.nu ['kv set', 'kv get']
|
||||
use std-rfc/kv ['kv set', 'kv get']
|
||||
|
||||
# Commonly used exit codes
|
||||
export const ECODE = {
|
||||
@@ -18,6 +18,8 @@ export const ECODE = {
|
||||
CONDITION_NOT_SATISFIED: 8,
|
||||
}
|
||||
|
||||
export const GITHUB_API_BASE = 'https://api.github.com'
|
||||
|
||||
# If current host is Windows
|
||||
export def windows? [] {
|
||||
# Windows / Darwin
|
||||
@@ -49,8 +51,8 @@ export def compare-ver [v1: string, v2: string] {
|
||||
# If you want to compare more parts use the following code:
|
||||
# for i in 0..([2 ($a | length) ($b | length)] | math max)
|
||||
for i in 0..2 {
|
||||
let x = $a | get -i $i | default 0
|
||||
let y = $b | get -i $i | default 0
|
||||
let x = $a | get -o $i | default 0
|
||||
let y = $b | get -o $i | default 0
|
||||
if $x > $y { return 1 }
|
||||
if $x < $y { return (-1) }
|
||||
}
|
||||
@@ -79,28 +81,69 @@ export def check-nushell [--debug] {
|
||||
}
|
||||
|
||||
# Converts a .env file into a record
|
||||
# may be used like this: open .env | load-env
|
||||
# works with quoted and unquoted .env files
|
||||
export def 'from env' []: string -> record {
|
||||
lines
|
||||
| split column '#' # remove comments
|
||||
| get column1
|
||||
| parse '{key}={value}'
|
||||
| update value {
|
||||
str trim # Trim whitespace between value and inline comments
|
||||
| str trim -c '"' # unquote double-quoted values
|
||||
| str trim -c "'" # unquote single-quoted values
|
||||
| str replace -a "\\n" "\n" # replace `\n` with newline char
|
||||
| str replace -a "\\r" "\r" # replace `\r` with carriage return
|
||||
| str replace -a "\\t" "\t" # replace `\t` with tab
|
||||
}
|
||||
| transpose -r -d
|
||||
# May be used like this: open .env | load-env
|
||||
# Works with quoted and unquoted .env files
|
||||
export def "from env" []: string -> record {
|
||||
let input = $in
|
||||
|
||||
# Process escape sequences in double-quoted values using str replace chain
|
||||
# Use NUL char as placeholder to avoid replacement conflicts
|
||||
let process_escapes = {|content: string|
|
||||
$content
|
||||
| str replace -a '\\' (char nul) # Placeholder for \\ to avoid conflicts
|
||||
| str replace -a '\n' (char nl)
|
||||
| str replace -a '\r' (char cr)
|
||||
| str replace -a '\t' (char tab)
|
||||
| str replace -a '\"' '"'
|
||||
| str replace -a (char nul) '\' # Restore \\ to single \
|
||||
}
|
||||
|
||||
# Parse double-quoted value with escape sequence support
|
||||
let parse_double_quoted = {|val: string|
|
||||
let matched = ($val | parse -r '^"(?P<content>(?:[^"\\]|\\.)*)"')
|
||||
if ($matched | is-empty) { $val | str trim -c '"' } else { do $process_escapes $matched.0.content }
|
||||
}
|
||||
|
||||
# Parse single-quoted value (no escape processing)
|
||||
let parse_single_quoted = {|val: string|
|
||||
let matched = ($val | parse -r "^'(?P<content>[^']*)'")
|
||||
if ($matched | is-empty) { $val | str trim -c "'" } else { $matched.0.content }
|
||||
}
|
||||
|
||||
# Parse unquoted value: handle escaped hash (\#) and strip inline comments
|
||||
let parse_unquoted = {|val: string|
|
||||
$val
|
||||
| str replace -a '\#' (char nul) # Placeholder for \#
|
||||
| split row '#' # Split by comment delimiter
|
||||
| first # Take content before first #
|
||||
| str replace -a (char nul) '#' # Restore \# to #
|
||||
| str trim
|
||||
}
|
||||
|
||||
# Parse value based on its format
|
||||
let parse_value = {|val: string|
|
||||
match $val {
|
||||
$v if ($v | str starts-with '"') => { do $parse_double_quoted $v }
|
||||
$v if ($v | str starts-with "'") => { do $parse_single_quoted $v }
|
||||
_ => { do $parse_unquoted $val }
|
||||
}
|
||||
}
|
||||
|
||||
let parsed = $input | lines
|
||||
| str trim
|
||||
| compact -e
|
||||
| where {|line| not ($line | str starts-with '#') }
|
||||
| parse "{key}={value}"
|
||||
| update key {|row| $row.key | str trim | str replace -r '^export\s+' '' }
|
||||
| update value {|row| do $parse_value ($row.value | str trim) }
|
||||
|
||||
if ($parsed | is-empty) { {} } else { $parsed | transpose -r -d -l }
|
||||
}
|
||||
|
||||
# Compact the record by removing empty columns
|
||||
export def compact-record []: record -> record {
|
||||
let record = $in
|
||||
let empties = $record | columns | filter {|it| $record | get $it | is-empty }
|
||||
let empties = $record | columns | where {|it| $record | get $it | is-empty }
|
||||
$record | reject ...$empties
|
||||
}
|
||||
|
||||
@@ -150,7 +193,8 @@ export def git-check [
|
||||
# Check if current directory is a git repo
|
||||
export def is-repo [] {
|
||||
let checkRepo = try {
|
||||
do -i { git rev-parse --is-inside-work-tree } | complete
|
||||
# Put `complete` inside `do` block to avoid pipefail error in Nushell 0.110+
|
||||
do { git rev-parse --is-inside-work-tree | complete }
|
||||
} catch {
|
||||
({ stdout: 'false' })
|
||||
}
|
||||
@@ -162,13 +206,13 @@ export def has-ref [
|
||||
ref: string # The git ref to check
|
||||
] {
|
||||
if not (is-repo) { return false }
|
||||
# Brackets were required here, or error will occur
|
||||
let parse = (do -i { git rev-parse --verify -q $ref } | complete)
|
||||
# Put `complete` inside `do` block to avoid pipefail error in Nushell 0.110+
|
||||
let parse = (do { git rev-parse --verify -q $ref | complete })
|
||||
if ($parse.stdout | is-empty) { false } else { true }
|
||||
}
|
||||
|
||||
# Notify the user that the `CHAT_TOKEN` hasn't been configured
|
||||
export const NO_TOKEN_TIP = (
|
||||
"**Notice:** It looks like you're using [`hustcer/deepseek-review`](https://github.com/hustcer/deepseek-review), but the `CHAT_TOKEN` hasn't" +
|
||||
"been configured in your repo's Variables/Secrets. Please ensure this token is set for proper functionality. For step-by-step guidance, refer" +
|
||||
"**Notice:** It looks like you're using [`hustcer/deepseek-review`](https://github.com/hustcer/deepseek-review), but the `CHAT_TOKEN` hasn't " +
|
||||
"been configured in your repo's **Variables/Secrets**. Please ensure this token is set for proper functionality. For step-by-step guidance, refer " +
|
||||
"to the **CHAT_TOKEN Config** section of [README](https://github.com/hustcer/deepseek-review/blob/main/README.md#code-review-with-github-action).")
|
||||
|
||||
23
nu/config.nu
23
nu/config.nu
@@ -28,15 +28,15 @@ def check-prompts [options: record] {
|
||||
|
||||
# Check if the specified type of prompt key exists in the config.yml file
|
||||
def check-prompt [options: record, type: string] {
|
||||
let prompt_key = $options.settings | get -i $'($type)-prompt' | default ''
|
||||
let prompt_key = $options.settings | get -o $'($type)-prompt' | default ''
|
||||
if ($prompt_key | is-empty) {
|
||||
print $'(ansi r)The ($type) prompt key is missing in `settings.($type)-prompt` config.yml file.(ansi reset)'
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
let prompt = $options.prompts | get -i $type
|
||||
let prompt = $options.prompts | get -o $type
|
||||
| default []
|
||||
| where name == $prompt_key
|
||||
| get -i 0.prompt
|
||||
| get -o 0.prompt
|
||||
if ($prompt | is-empty) {
|
||||
print $'The ($type) prompt (ansi r)($prompt_key)(ansi reset) is missing in `prompts.($type)` of config.yml file.'
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
@@ -59,11 +59,11 @@ def check-providers [options: record] {
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
# Each provider should have name, token and models field
|
||||
$options.providers | each {|it|
|
||||
let empties = [name token models] | filter { |field| $it | get -i $field | is-empty }
|
||||
$options.providers | each {|p|
|
||||
let empties = [name token models] | where { |field| $p | get -o $field | is-empty }
|
||||
if ($empties | is-not-empty) {
|
||||
print $'Field (ansi r)`($empties | str join ,)`(ansi reset) should not be empty for provider:'
|
||||
$it | table -e -t psql | print
|
||||
$p | table -e -t psql | print
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,8 @@ def check-models [options: record] {
|
||||
}
|
||||
|
||||
# Check if the config.yml file exists and if it's valid
|
||||
export def config-check [--config: string = $SETTING_FILE] {
|
||||
export def config-check [--config: string] {
|
||||
let config = $config | default $SETTING_FILE
|
||||
file-exists $config
|
||||
let options = open $config
|
||||
check-prompts $options
|
||||
@@ -105,7 +106,7 @@ def get-model-envs [settings: record, model?: string = ''] {
|
||||
let provider = $settings.providers
|
||||
| default []
|
||||
| where name == $name
|
||||
| get -i 0
|
||||
| get -o 0
|
||||
| default {}
|
||||
let model_name = $provider.models
|
||||
| default []
|
||||
@@ -114,7 +115,7 @@ def get-model-envs [settings: record, model?: string = ''] {
|
||||
} else {
|
||||
$it.name == $model or $it.alias? == $model }
|
||||
}
|
||||
| get -i 0.name
|
||||
| get -o 0.name
|
||||
| default $model
|
||||
|
||||
{ CHAT_TOKEN: $provider.token?, BASE_URL: $provider.base-url?, CHAT_URL: $provider.chat-url?, CHAT_MODEL: $model_name }
|
||||
@@ -132,12 +133,12 @@ export def --env config-load [
|
||||
let user_prompt = $all_settings.prompts?.user?
|
||||
| default []
|
||||
| where name == ($settings.user-prompt? | default '')
|
||||
| get -i 0.prompt
|
||||
| get -o 0.prompt
|
||||
|
||||
let system_prompt = $all_settings.prompts?.system?
|
||||
| default []
|
||||
| where name == ($settings.system-prompt? | default '')
|
||||
| get -i 0.prompt
|
||||
| get -o 0.prompt
|
||||
|
||||
let model_envs = get-model-envs $all_settings $model
|
||||
|
||||
|
||||
157
nu/diff.nu
Normal file
157
nu/diff.nu
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env nu
|
||||
# Author: hustcer
|
||||
# Created: 2025/04/02 20:02:15
|
||||
# Description: Diff command for DeepSeek-Review
|
||||
|
||||
use common.nu [GITHUB_API_BASE, ECODE, git-check, has-ref]
|
||||
use util.nu [generate-include-regex, generate-exclude-regex, prepare-awk, is-safe-git]
|
||||
|
||||
# If the PR title or body contains any of these keywords, skip the review
|
||||
const IGNORE_REVIEW_KEYWORDS = ['skip review' 'skip cr']
|
||||
|
||||
# Get the diff content from GitHub PR or local git changes and apply filters
|
||||
export def get-diff [
|
||||
--repo: string, # GitHub repository name
|
||||
--pr-number: string, # GitHub PR number
|
||||
--diff-to: string, # Diff to git ref
|
||||
--diff-from: string, # Diff from git ref
|
||||
--include: string, # Comma separated file patterns to include in the code review
|
||||
--exclude: string, # Comma separated file patterns to exclude in the code review
|
||||
--patch-cmd: string, # The `git show` or `git diff` command to get the diff content
|
||||
] {
|
||||
let content = (
|
||||
get-diff-content --repo $repo --pr-number $pr_number --patch-cmd $patch_cmd
|
||||
--diff-to $diff_to --diff-from $diff_from --include $include --exclude $exclude)
|
||||
|
||||
if ($content | is-empty) {
|
||||
print $'(ansi g)Nothing to review.(ansi reset)'
|
||||
exit $ECODE.SUCCESS
|
||||
}
|
||||
|
||||
apply-file-filters $content --include $include --exclude $exclude
|
||||
}
|
||||
|
||||
# Get diff content from GitHub PR or local git changes
|
||||
def get-diff-content [
|
||||
--repo: string, # GitHub repository name
|
||||
--pr-number: string, # GitHub PR number
|
||||
--diff-to: string, # Diff to git ref
|
||||
--diff-from: string, # Diff from git ref
|
||||
--include: string, # Comma separated file patterns to include in the code review
|
||||
--exclude: string, # Comma separated file patterns to exclude in the code review
|
||||
--patch-cmd: string, # The `git show` or `git diff` command to get the diff content
|
||||
] {
|
||||
let local_repo = $env.PWD
|
||||
|
||||
if ($pr_number | is-not-empty) {
|
||||
get-pr-diff --repo $repo $pr_number
|
||||
} else if ($diff_from | is-not-empty) {
|
||||
get-ref-diff $diff_from --diff-to $diff_to
|
||||
} else if not (git-check $local_repo --check-repo=1) {
|
||||
print $'Current directory ($local_repo) is (ansi r)NOT(ansi reset) a git repo, bye...(char nl)'
|
||||
exit $ECODE.CONDITION_NOT_SATISFIED
|
||||
} else if ($patch_cmd | is-not-empty) {
|
||||
get-patch-diff $patch_cmd
|
||||
} else {
|
||||
git diff
|
||||
}
|
||||
}
|
||||
|
||||
# Get the diff content of the specified GitHub PR,
|
||||
# if the PR description contains the skip keyword, exit
|
||||
def get-pr-diff [
|
||||
--repo: string, # GitHub repository name
|
||||
pr_number: string, # GitHub PR number
|
||||
] {
|
||||
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json]
|
||||
let DIFF_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3.diff]
|
||||
|
||||
if ($repo | is-empty) {
|
||||
print $'(ansi r)Please provide the GitHub repository name by `--repo` option.(ansi reset)'
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
|
||||
let description = http get -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)'
|
||||
| select title body | values | str join "\n"
|
||||
|
||||
# Check if the PR title or body contains keywords to skip the review
|
||||
if ($IGNORE_REVIEW_KEYWORDS | any {|it| $description =~ $it }) {
|
||||
print $'(ansi r)The PR title or body contains keywords to skip the review, bye...(ansi reset)'
|
||||
exit $ECODE.SUCCESS
|
||||
}
|
||||
|
||||
let commit_msg = http get -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)/commits'
|
||||
| last | get commit.message
|
||||
if ($IGNORE_REVIEW_KEYWORDS | any {|it| $commit_msg =~ $it }) {
|
||||
print $'(ansi r)The latest PR commit message contains keywords to skip the review, bye...(ansi reset)'
|
||||
exit $ECODE.SUCCESS
|
||||
}
|
||||
|
||||
# Get the diff content of the PR
|
||||
http get -H $DIFF_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)' | str trim
|
||||
}
|
||||
|
||||
# Get diff content from local git changes
|
||||
def get-ref-diff [
|
||||
diff_from: string, # Diff from git REF
|
||||
--diff-to: string, # Diff to git ref
|
||||
] {
|
||||
# Validate the git refs
|
||||
if not (has-ref $diff_from) {
|
||||
print $'(ansi r)The specified git ref ($diff_from) does not exist, please check it again.(ansi reset)'
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
|
||||
if ($diff_to | is-not-empty) and not (has-ref $diff_to) {
|
||||
print $'(ansi r)The specified git ref ($diff_to) does not exist, please check it again.(ansi reset)'
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
|
||||
git diff $diff_from ($diff_to | default HEAD)
|
||||
}
|
||||
|
||||
# Get the diff content from the specified git command
|
||||
def get-patch-diff [
|
||||
cmd: string # The `git show` or `git diff` command to get the diff content
|
||||
] {
|
||||
let valid = is-safe-git $cmd
|
||||
if not $valid {
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
|
||||
# Get the diff content from the specified git command
|
||||
nu -c $cmd
|
||||
}
|
||||
|
||||
# Apply file filters to the diff content to include or exclude specific files
|
||||
def apply-file-filters [
|
||||
content: string, # The diff content to filter
|
||||
--include: string, # Comma separated file patterns to include in the code review
|
||||
--exclude: string, # Comma separated file patterns to exclude in the code review
|
||||
] {
|
||||
mut filtered_content = $content
|
||||
let awk_bin = (prepare-awk)
|
||||
let outdated_awk = $'If you are using an (ansi r)outdated awk version(ansi reset), please upgrade to the latest version or use gawk latest instead.'
|
||||
|
||||
if ($include | is-not-empty) {
|
||||
let patterns = $include | split row ','
|
||||
$filtered_content = $filtered_content | try {
|
||||
^$awk_bin (generate-include-regex $patterns)
|
||||
} catch {
|
||||
print $outdated_awk
|
||||
exit $ECODE.OUTDATED
|
||||
}
|
||||
}
|
||||
|
||||
if ($exclude | is-not-empty) {
|
||||
let patterns = $exclude | split row ','
|
||||
$filtered_content = $filtered_content | try {
|
||||
^$awk_bin (generate-exclude-regex $patterns)
|
||||
} catch {
|
||||
print $outdated_awk
|
||||
exit $ECODE.OUTDATED
|
||||
}
|
||||
}
|
||||
|
||||
$filtered_content
|
||||
}
|
||||
187
nu/kv.nu
187
nu/kv.nu
@@ -1,187 +0,0 @@
|
||||
# NOTE: This file was copied from https://github.com/nushell/nushell/tree/main/crates/nu-std/std-rfc/kv
|
||||
# And will be removed after the Nu v0.103 release
|
||||
# More examples could be found here: https://github.com/nushell/nushell/discussions/14965
|
||||
#
|
||||
# kv module
|
||||
#
|
||||
# use std-rfc/kv *
|
||||
#
|
||||
# Easily store and retrieve key-value pairs
|
||||
# in a pipeline.
|
||||
#
|
||||
# A common request is to be able to assign a
|
||||
# pipeline result to a variable. While it's
|
||||
# not currently possible to use a "let" statement
|
||||
# within a pipeline, this module provides an
|
||||
# alternative. Think of each key as a variable
|
||||
# that can be set and retrieved.
|
||||
|
||||
# Stores the pipeline value for later use
|
||||
#
|
||||
# If the key already exists, it is updated to the new value provided.
|
||||
export def "kv set" [
|
||||
key: string
|
||||
value_or_closure?: any
|
||||
--return (-r): string # Whether and what to return to the pipeline output
|
||||
--universal (-u)
|
||||
] {
|
||||
# Pipeline input is preferred, but prioritize
|
||||
# parameter if present. This allows $in to be
|
||||
# used in the parameter if needed.
|
||||
let input = $in
|
||||
|
||||
# If passed a closure, execute it
|
||||
let arg_type = ($value_or_closure | describe)
|
||||
let value = match $arg_type {
|
||||
closure => { $input | do $value_or_closure }
|
||||
_ => ($value_or_closure | default $input)
|
||||
}
|
||||
|
||||
# Store values as nuons for type-integrity
|
||||
let kv_pair = {
|
||||
session: '' # Placeholder
|
||||
key: $key
|
||||
value: ($value | to nuon)
|
||||
}
|
||||
|
||||
let db_open = (db_setup --universal=$universal)
|
||||
try {
|
||||
# Delete the existing key if it does exist
|
||||
do $db_open | query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $key }
|
||||
}
|
||||
|
||||
match $universal {
|
||||
true => { $kv_pair | into sqlite (universal_db_path) -t std_kv_store }
|
||||
false => { $kv_pair | stor insert -t std_kv_store }
|
||||
}
|
||||
|
||||
# The value that should be returned from `kv set`
|
||||
# By default, this is the input to `kv set`, even if
|
||||
# overridden by a positional parameter.
|
||||
# This can also be:
|
||||
# input: (Default) The pipeline input to `kv set`, even if
|
||||
# overridden by a positional parameter. `null` if no
|
||||
# pipeline input was used.
|
||||
# ---
|
||||
# value: If a positional parameter was used for the value, then
|
||||
# return it, otherwise return the input (whatever was set).
|
||||
# If the positional was a closure, return the result of the
|
||||
# closure on the pipeline input.
|
||||
# ---
|
||||
# all: The entire contents of the existing kv table are returned
|
||||
match ($return | default 'input') {
|
||||
'all' => (kv list --universal=$universal)
|
||||
'a' => (kv list --universal=$universal)
|
||||
'value' => $value
|
||||
'v' => $value
|
||||
'input' => $input
|
||||
'in' => $input
|
||||
'i' => $input
|
||||
_ => {
|
||||
error make {
|
||||
msg: "Invalid --return option"
|
||||
label: {
|
||||
text: "Must be 'all'/'a', 'value'/'v', or 'input'/'in'/'i'"
|
||||
span: (metadata $return).span
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Retrieves a stored value by key
|
||||
#
|
||||
# Counterpart of "kv set". Returns null if the key is not found.
|
||||
export def "kv get" [
|
||||
key: string # Key of the kv-pair to retrieve
|
||||
--universal (-u)
|
||||
] {
|
||||
let db_open = (db_setup --universal=$universal)
|
||||
do $db_open
|
||||
# Hack to turn a SQLiteDatabase into a table
|
||||
| $in.std_kv_store | wrap temp | get temp
|
||||
| where key == $key
|
||||
# Should only be one occurrence of each key in the stor
|
||||
| get -i value.0
|
||||
| match $in {
|
||||
# Key not found
|
||||
null => null
|
||||
# Key found
|
||||
_ => { from nuon }
|
||||
}
|
||||
}
|
||||
|
||||
# List the currently stored key-value pairs
|
||||
#
|
||||
# Returns results as the Nushell value rather than the stored nuon.
|
||||
export def "kv list" [
|
||||
--universal (-u)
|
||||
] {
|
||||
let db_open = (db_setup --universal=$universal)
|
||||
|
||||
do $db_open | $in.std_kv_store? | each {|kv_pair|
|
||||
{
|
||||
key: $kv_pair.key
|
||||
value: ($kv_pair.value | from nuon )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Returns and removes a key-value pair
|
||||
export def --env "kv drop" [
|
||||
key: string # Key of the kv-pair to drop
|
||||
--universal (-u)
|
||||
] {
|
||||
let db_open = (db_setup --universal=$universal)
|
||||
|
||||
let value = (kv get --universal=$universal $key)
|
||||
|
||||
try {
|
||||
do $db_open
|
||||
# Hack to turn a SQLiteDatabase into a table
|
||||
| query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $key }
|
||||
}
|
||||
|
||||
if $universal and ($env.NU_KV_UNIVERSALS? | default false) {
|
||||
hide-env $key
|
||||
}
|
||||
|
||||
$value
|
||||
}
|
||||
|
||||
def universal_db_path [] {
|
||||
$env.NU_UNIVERSAL_KV_PATH?
|
||||
| default (
|
||||
$nu.data-dir | path join "std_kv_variables.sqlite3"
|
||||
)
|
||||
}
|
||||
|
||||
def db_setup [
|
||||
--universal
|
||||
] : nothing -> closure {
|
||||
try {
|
||||
match $universal {
|
||||
true => {
|
||||
# Ensure universal sqlite db and table exists
|
||||
let uuid = (random uuid)
|
||||
let dummy_record = {
|
||||
session: ''
|
||||
key: $uuid
|
||||
value: ''
|
||||
}
|
||||
$dummy_record | into sqlite (universal_db_path) -t std_kv_store
|
||||
open (universal_db_path) | query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $uuid }
|
||||
}
|
||||
false => {
|
||||
# Create the stor table if it doesn't exist
|
||||
stor create -t std_kv_store -c {session: str, key: str, value: str} | ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Return the correct closure for opening on-disk vs. in-memory
|
||||
match $universal {
|
||||
true => {{|| open (universal_db_path)}}
|
||||
false => {{|| stor open}}
|
||||
}
|
||||
}
|
||||
@@ -46,9 +46,9 @@ export def 'make-release' [
|
||||
export def has-ref [
|
||||
ref: string # The git ref to check
|
||||
] {
|
||||
let checkRepo = (do -i { git rev-parse --is-inside-work-tree } | complete)
|
||||
# Put `complete` inside `do` block to avoid pipefail error in Nushell 0.110+
|
||||
let checkRepo = (do { git rev-parse --is-inside-work-tree | complete })
|
||||
if not ($checkRepo.stdout =~ 'true') { return false }
|
||||
# Brackets were required here, or error will occur
|
||||
let parse = (do -i { git rev-parse --verify -q $ref } | complete)
|
||||
let parse = (do { git rev-parse --verify -q $ref | complete })
|
||||
if ($parse.stdout | is-empty) { false } else { true }
|
||||
}
|
||||
|
||||
304
nu/review.nu
Normal file → Executable file
304
nu/review.nu
Normal file → Executable file
@@ -25,64 +25,69 @@
|
||||
# - Local Repo Review: just cr -f HEAD~1 --debug
|
||||
# - Local PR Review: just cr -r hustcer/deepseek-review -n 32
|
||||
|
||||
use kv.nu *
|
||||
use std-rfc/kv *
|
||||
use diff.nu [get-diff]
|
||||
use common.nu [
|
||||
ECODE, NO_TOKEN_TIP, hr-line, is-installed, windows?, mac?,
|
||||
compare-ver, compact-record, git-check, has-ref,
|
||||
compare-ver, compact-record, git-check, has-ref, GITHUB_API_BASE
|
||||
]
|
||||
|
||||
const RESPONSE_END = 'data: [DONE]'
|
||||
|
||||
const GITHUB_API_BASE = 'https://api.github.com'
|
||||
const IGNORED_MESSAGES = {
|
||||
'-alive': true, # The server is alive
|
||||
'data: [DONE]': true, # The end of the response
|
||||
': OPENROUTER PROCESSING': true, # OPENROUTER in PROCESSING message
|
||||
}
|
||||
|
||||
# It takes longer to respond to requests made with unknown/rare user agents.
|
||||
# When make http post pretend to be curl, it gets a response just as quickly as curl.
|
||||
const HTTP_HEADERS = [User-Agent curl/8.9]
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
MODEL: 'deepseek-chat',
|
||||
TEMPERATURE: 1.0,
|
||||
MODEL: 'deepseek-v4-flash',
|
||||
TEMPERATURE: 0.3,
|
||||
BASE_URL: 'https://api.deepseek.com',
|
||||
USER_PROMPT: 'Please review the following code changes:',
|
||||
SYS_PROMPT: 'You are a professional code review assistant responsible for analyzing code changes in GitHub Pull Requests. Identify potential issues such as code style violations, logical errors, security vulnerabilities, and provide improvement suggestions. Clearly list the problems and recommendations in a concise manner.',
|
||||
}
|
||||
|
||||
# If the PR title or body contains any of these keywords, skip the review
|
||||
const IGNORE_REVIEW_KEYWORDS = ['skip review' 'skip cr']
|
||||
|
||||
# Use DeepSeek AI to review code changes locally or in GitHub Actions
|
||||
export def --env deepseek-review [
|
||||
token?: string, # Your DeepSeek API token, fallback to CHAT_TOKEN env var
|
||||
--debug(-d), # Debug mode
|
||||
--repo(-r): string, # GitHub repo name, e.g. hustcer/deepseek-review, or local repo path / alias
|
||||
--output(-o): string, # Output file path
|
||||
--pr-number(-n): string, # GitHub PR number
|
||||
--gh-token(-k): string, # Your GitHub token, fallback to GITHUB_TOKEN env var
|
||||
--diff-to(-t): string, # Diff to git REF
|
||||
--diff-from(-f): string, # Diff from git REF
|
||||
--diff-to(-t): string, # Git diff ending commit SHA
|
||||
--diff-from(-f): string, # Git diff starting commit SHA
|
||||
--patch-cmd(-c): string, # The `git show` or `git diff` command to get the diff content, for local CR only
|
||||
--max-length(-l): int, # Maximum length of the content for review, 0 means no limit.
|
||||
--model(-m): string, # Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
|
||||
--model(-m): string, # Model name, or read from CHAT_MODEL env var, `deepseek-v4-flash` by default
|
||||
--base-url(-b): string, # DeepSeek API base URL, fallback to BASE_URL env var
|
||||
--chat-url(-U): string, # DeepSeek Model chat full API URL, e.g. http://localhost:11535/api/chat
|
||||
--sys-prompt(-s): string # Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||
--include(-i): string, # Comma separated file patterns to include in the code review
|
||||
--exclude(-x): string, # Comma separated file patterns to exclude in the code review
|
||||
--temperature(-T): float, # Temperature for the model, between `0` and `2`, default value `1.0`
|
||||
--temperature(-T): float, # Temperature for the model, between `0` and `2`, default value `0.3`
|
||||
]: nothing -> nothing {
|
||||
|
||||
$env.config.table.mode = 'psql'
|
||||
let local_repo = $env.PWD
|
||||
let write_file = ($output | is-not-empty)
|
||||
let is_action = ($env.GITHUB_ACTIONS? == 'true')
|
||||
let stream = if $is_action { false } else { true }
|
||||
let token = $token | default $env.CHAT_TOKEN?
|
||||
let repo = $repo | default $env.DEFAULT_GITHUB_REPO?
|
||||
let CHAT_HEADER = [Authorization $'Bearer ($token)']
|
||||
let stream = if $is_action or $write_file { false } else { true }
|
||||
let model = $model | default $env.CHAT_MODEL? | default $DEFAULT_OPTIONS.MODEL
|
||||
let base_url = $base_url | default $env.BASE_URL? | default $DEFAULT_OPTIONS.BASE_URL
|
||||
let url = $chat_url | default $env.CHAT_URL? | default $'($base_url)/chat/completions'
|
||||
let max_length = try { $max_length | default ($env.MAX_LENGTH? | default 0 | into int) } catch { 0 }
|
||||
let temperature = try { $temperature | default $env.TEMPERATURE? | default $DEFAULT_OPTIONS.TEMPERATURE | into float } catch { $DEFAULT_OPTIONS.TEMPERATURE }
|
||||
# Determine output mode
|
||||
let output_mode = if $is_action { 'action' } else if ($output | is-not-empty) { 'file' } else { 'console' }
|
||||
|
||||
validate-temperature $temperature
|
||||
let setting = {
|
||||
repo: $repo,
|
||||
@@ -109,7 +114,7 @@ export def --env deepseek-review [
|
||||
print $hint; print -n (char nl)
|
||||
if ($pr_number | is-empty) {
|
||||
print 'Current Settings:'; hr-line
|
||||
$setting | compact-record | reject -i repo | print; print -n (char nl)
|
||||
$setting | compact-record | reject -o repo | print; print -n (char nl)
|
||||
}
|
||||
|
||||
let content = (
|
||||
@@ -130,7 +135,8 @@ export def --env deepseek-review [
|
||||
messages: [
|
||||
{ role: 'system', content: $sys_prompt },
|
||||
{ role: 'user', content: $"($user_prompt):\n($content)" }
|
||||
]
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
}
|
||||
if $debug { print $'(char nl)Code Changes:'; hr-line; print $content }
|
||||
print $'(char nl)Waiting for response from (ansi g)($url)(ansi reset) ...'
|
||||
@@ -146,26 +152,59 @@ export def --env deepseek-review [
|
||||
print $'✖️ Code review failed!Error: '; hr-line; print $response
|
||||
exit $ECODE.SERVER_ERROR
|
||||
}
|
||||
let reason = $response | get -i choices.0.message.reasoning_content
|
||||
let review = $response | get -i choices.0.message.content | default ($response | get -i message.content)
|
||||
let message = $response | get -o choices.0.message
|
||||
let reason = $message | coalesce-reasoning
|
||||
let review = $message.content? | default ($response | get -o message.content)
|
||||
let result = ['<details>' '<summary> Reasoning Details</summary>' $reason "</details>\n" $review] | str join "\n"
|
||||
if ($review | is-empty) {
|
||||
print $'✖️ Code review failed!No review result returned from ($base_url) ...'
|
||||
exit $ECODE.SERVER_ERROR
|
||||
}
|
||||
let result = if ($reason | is-empty) { $review } else { $result }
|
||||
if not $is_action {
|
||||
print $'Code Review Result:'; hr-line; print $result
|
||||
} else {
|
||||
post-comments-to-pr $repo $pr_number $result
|
||||
print $'✅ Code review finished!PR (ansi g)#($pr_number)(ansi reset) review result was posted as a comment.'
|
||||
|
||||
match $output_mode {
|
||||
'action' => {
|
||||
post-comments-to-pr $repo $pr_number $result
|
||||
print $'✅ Code review finished!PR (ansi g)#($pr_number)(ansi reset) review result was posted as a comment.'
|
||||
}
|
||||
'file' => { write-review-to-file $output $setting $result $response }
|
||||
_ => { print $'Code Review Result:'; hr-line; print $result }
|
||||
}
|
||||
|
||||
if ($response.usage? | is-not-empty) {
|
||||
print $'(char nl)Token Usage:'; hr-line
|
||||
$response.usage? | table -e | print
|
||||
}
|
||||
}
|
||||
|
||||
# Write the code review result to a file
|
||||
def write-review-to-file [
|
||||
file: string, # Output file path
|
||||
setting: record, # Review settings
|
||||
result: string, # Review result
|
||||
response: record, # DeepSeek API response
|
||||
] {
|
||||
let file = (if not ($file | str ends-with '.md') { $'($file).md' } else { $file })
|
||||
let token_usage = if ($response.usage? | is-empty) { [] } else {
|
||||
['## Token Usage', '', ($response.usage? | transpose key val | to md --pretty)]
|
||||
}
|
||||
# Generate content sections
|
||||
let content_sections = [
|
||||
'# DeepSeek Code Review Result', ''
|
||||
$"Generated at: (date now | format date '%Y/%m/%d %H:%M:%S')", ''
|
||||
'## Code Review Settings', ''
|
||||
($setting | compact-record | reject -o repo | transpose key val | to md --pretty)
|
||||
'', '## Review Detail', '', $result, '', ...$token_usage
|
||||
]
|
||||
try {
|
||||
$content_sections | str join (char nl) | save --force $file
|
||||
print $'Code Review Result saved to (ansi g)($file)(ansi reset)'
|
||||
} catch {|err|
|
||||
print $'(ansi r)Failed to save review result: (ansi reset)'
|
||||
$err | table -e | print
|
||||
}
|
||||
}
|
||||
|
||||
# Validate the DeepSeek API token
|
||||
def validate-token [token?: string, --pr-number: string, --repo: string] {
|
||||
if ($token | is-empty) {
|
||||
@@ -225,219 +264,46 @@ def streaming-output [
|
||||
}
|
||||
| try { lines } catch { print $'(ansi r)Error Happened ...(ansi reset)'; exit $ECODE.SERVER_ERROR }
|
||||
| each {|line|
|
||||
if $line == $RESPONSE_END { return }
|
||||
if ($line | is-empty) { return }
|
||||
# DeepSeek Response vs Local Ollama Response
|
||||
let $last = if $line =~ '^data: ' { $line | str substring 6.. | from json } else { $line | from json }
|
||||
if $last == '-alive' { print $last; return }
|
||||
if ($IGNORED_MESSAGES | get -o $line | default false) { return }
|
||||
let $last = $line | parse-line
|
||||
if $debug { $last | to json | kv set last-reply }
|
||||
$last | get -i choices.0.delta | default ($last | get -i message) | if ($in | is-not-empty) {
|
||||
$last | get -o choices.0.delta | default ($last | get -o message) | if ($in | is-not-empty) {
|
||||
let delta = $in
|
||||
if ($delta.reasoning_content? | is-not-empty) { kv set reasoning ((kv get reasoning) + 1) }
|
||||
if ($delta | coalesce-reasoning | is-not-empty) { kv set reasoning ((kv get reasoning) + 1) }
|
||||
if (kv get reasoning) == 1 { print $'(char nl)Reasoning Details:'; hr-line }
|
||||
if ($delta.content | is-not-empty) { kv set content ((kv get content) + 1) }
|
||||
if (kv get content) == 1 { print $'(char nl)Review Details:'; hr-line }
|
||||
print -n ($delta.reasoning_content? | default $delta.content)
|
||||
print -n ($delta | coalesce-reasoning | default $delta.content)
|
||||
}
|
||||
}
|
||||
|
||||
if $debug and (kv get last-reply | is-not-empty) {
|
||||
print $'(char nl)(char nl)Model & Token Usage:'; hr-line
|
||||
kv get last-reply | from json | select -i model usage | table -e | print
|
||||
kv get last-reply | from json | select -o model usage | table -e | print
|
||||
}
|
||||
}
|
||||
|
||||
# Get the diff content from GitHub PR or local git changes
|
||||
export def get-diff [
|
||||
--repo: string, # GitHub repository name
|
||||
--pr-number: string, # GitHub PR number
|
||||
--diff-to: string, # Diff to git ref
|
||||
--diff-from: string, # Diff from git ref
|
||||
--include: string, # Comma separated file patterns to include in the code review
|
||||
--exclude: string, # Comma separated file patterns to exclude in the code review
|
||||
--patch-cmd: string, # The `git show` or `git diff` command to get the diff content
|
||||
] {
|
||||
let local_repo = $env.PWD
|
||||
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json]
|
||||
let DIFF_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3.diff]
|
||||
mut content = if ($pr_number | is-not-empty) {
|
||||
if ($repo | is-empty) {
|
||||
print $'(ansi r)Please provide the GitHub repository name by `--repo` option.(ansi reset)'
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
# Parse the line from the streaming response
|
||||
def parse-line [] {
|
||||
let $line = $in
|
||||
# DeepSeek Response vs Local Ollama Response
|
||||
try {
|
||||
if $line =~ '^data: ' {
|
||||
$line | str substring 6.. | from json
|
||||
} else {
|
||||
$line | from json
|
||||
}
|
||||
# TODO: Ignore keywords checking when triggering by mentioning the bot
|
||||
let description = http get -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)'
|
||||
| select title body | values | str join "\n"
|
||||
if ($IGNORE_REVIEW_KEYWORDS | any {|it| $description =~ $it }) {
|
||||
print $'(ansi r)The PR title or body contains keywords to skip the review, bye...(ansi reset)'
|
||||
exit $ECODE.SUCCESS
|
||||
}
|
||||
http get -H $DIFF_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)' | str trim
|
||||
} else if ($diff_from | is-not-empty) {
|
||||
if not (has-ref $diff_from) {
|
||||
print $'(ansi r)The specified git ref ($diff_from) does not exist, please check it again.(ansi reset)'
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
if ($diff_to | is-not-empty) and not (has-ref $diff_to) {
|
||||
print $'(ansi r)The specified git ref ($diff_to) does not exist, please check it again.(ansi reset)'
|
||||
exit $ECODE.INVALID_PARAMETER
|
||||
}
|
||||
git diff $diff_from ($diff_to | default HEAD)
|
||||
} else if not (git-check $local_repo --check-repo=1) {
|
||||
print $'Current directory ($local_repo) is (ansi r)NOT(ansi reset) a git repo, bye...(char nl)'
|
||||
exit $ECODE.CONDITION_NOT_SATISFIED
|
||||
} else if ($patch_cmd | is-not-empty) {
|
||||
let valid = is-safe-git $patch_cmd
|
||||
if not $valid { exit $ECODE.INVALID_PARAMETER }
|
||||
nu -c $patch_cmd
|
||||
} else { git diff }
|
||||
|
||||
if ($content | is-empty) {
|
||||
print $'(ansi g)Nothing to review.(ansi reset)'; exit $ECODE.SUCCESS
|
||||
} catch {
|
||||
print -e $'(ansi r)Unrecognized content:(ansi reset) ($line)'
|
||||
exit $ECODE.SERVER_ERROR
|
||||
}
|
||||
let awk_bin = (prepare-awk)
|
||||
let outdated_awk = $'If you are using an (ansi r)outdated awk version(ansi reset), please upgrade to the latest version or use gawk latest instead.'
|
||||
if ($include | is-not-empty) {
|
||||
let patterns = $include | split row ','
|
||||
$content = $content | try { ^$awk_bin (generate-include-regex $patterns) } catch { print $outdated_awk; exit $ECODE.OUTDATED }
|
||||
}
|
||||
if ($exclude | is-not-empty) {
|
||||
let patterns = $exclude | split row ','
|
||||
$content = $content | try { ^$awk_bin (generate-exclude-regex $patterns) } catch { print $outdated_awk; exit $ECODE.OUTDATED }
|
||||
}
|
||||
$content
|
||||
}
|
||||
|
||||
# AWK family version check for both awk and gawk
|
||||
# awk: awk version 20250116 -> 20250116
|
||||
# gawk: GNU Awk 5.3.1, API 4.0, (GNU MPFR 4.2.1, GNU MP 6.3.0) -> 5.3.1
|
||||
def get-awk-ver [awk_bin: string] {
|
||||
^$awk_bin --version | lines | first | split row , | first | split row ' ' | last
|
||||
}
|
||||
|
||||
# Prepare gawk for macOS
|
||||
export def prepare-awk [] {
|
||||
const MIN_GAWK_VERSION = '5.3.1'
|
||||
const MIN_AWK_VERSION = '20250116'
|
||||
let awk_installed = is-installed awk
|
||||
let gawk_installed = is-installed gawk
|
||||
|
||||
if $awk_installed {
|
||||
let awk_version = get-awk-ver awk
|
||||
if (compare-ver $awk_version $MIN_AWK_VERSION) >= 0 {
|
||||
print $'Current awk version: ($awk_version)'
|
||||
return 'awk'
|
||||
}
|
||||
}
|
||||
if $gawk_installed {
|
||||
let gawk_version = get-awk-ver gawk
|
||||
if (compare-ver $gawk_version $MIN_GAWK_VERSION) >= 0 {
|
||||
print $'Current gawk version: ($gawk_version)'
|
||||
return 'gawk'
|
||||
} else if (windows?) and ($env.GITHUB_ACTIONS? == 'true') {
|
||||
let awk_info = (install-gawk-for-actions)
|
||||
print $'Current gawk version: ($awk_info.version)'
|
||||
return $awk_info.awk_bin
|
||||
}
|
||||
}
|
||||
if (mac?) and (is-installed brew) {
|
||||
brew install gawk
|
||||
print $'Current gawk version: (get-awk-ver gawk)'
|
||||
return 'gawk'
|
||||
}
|
||||
if (not $awk_installed) and (not $gawk_installed) {
|
||||
print $'(ansi r)Neither `awk` nor `gawk` is installed, please install the latest version of `gawk`.(ansi reset)'
|
||||
exit $ECODE.MISSING_BINARY
|
||||
}
|
||||
print $'Current awk version: (get-awk-ver awk)'
|
||||
'awk'
|
||||
}
|
||||
|
||||
# Convert glob patterns to regex patterns
|
||||
# Pass in *.nu directly as a regular expression does not work, because * in
|
||||
# a regular expression needs to be attached to the previous pattern, the correct
|
||||
# form should be .* So we should convert each glob pattern to a regex pattern:
|
||||
# 1. Convert * to .*
|
||||
# 2. Convert ? to . (optional, as needed)
|
||||
# 3. Convert / to \/
|
||||
def glob-to-regex [patterns: list<string>] {
|
||||
# Handle empty patterns list
|
||||
if ($patterns | length) == 0 { return '' }
|
||||
|
||||
# Define a mapping of characters to escape
|
||||
let regex_escapes = {
|
||||
# Escape special regex characters first
|
||||
"\\.": "\\\\.",
|
||||
"\\+": "\\\\+",
|
||||
"\\^": "\\\\^",
|
||||
"\\$": "\\\\$",
|
||||
"\\(": "\\\\(",
|
||||
"\\)": "\\\\)",
|
||||
"\\[": "\\\\[",
|
||||
"\\]": "\\\\]",
|
||||
"\\{": "\\\\{",
|
||||
"\\}": "\\\\}",
|
||||
"\\|": "\\\\|",
|
||||
# Then convert glob patterns to regex patterns
|
||||
"*": ".*",
|
||||
"?": ".",
|
||||
"/": "\\/",
|
||||
}
|
||||
|
||||
$patterns
|
||||
| each { |pat|
|
||||
$regex_escapes | columns | reduce -f $pat { |k, acc|
|
||||
$acc | str replace -a $k ($regex_escapes | get $k)
|
||||
}
|
||||
}
|
||||
| str join '|'
|
||||
}
|
||||
|
||||
# Generate the awk include regex pattern string for the specified patterns
|
||||
export def generate-include-regex [patterns: list<string>] {
|
||||
let pattern = glob-to-regex $patterns
|
||||
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}p"
|
||||
}
|
||||
|
||||
# Generate the awk exclude regex pattern string for the specified patterns
|
||||
export def generate-exclude-regex [patterns: list<string>] {
|
||||
let pattern = glob-to-regex $patterns
|
||||
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}!p"
|
||||
}
|
||||
|
||||
# Check if the git command is safe to run in the shell
|
||||
# Validate command examples:
|
||||
# - git show
|
||||
# - git diff
|
||||
# - git show head~1
|
||||
# - git diff 2393375 71f5a31
|
||||
# - git diff 2393375 71f5a31 nu/*
|
||||
# - git diff 2393375 71f5a31 :!nu/*
|
||||
export def is-safe-git [cmd: string] {
|
||||
let normalized_cmd = ($cmd | str trim | str downcase)
|
||||
|
||||
# Define allowed command patterns with named capture groups for better validation
|
||||
let git_cmd_pattern = '^git\s+(show|diff)(?:\s+(?:[a-zA-Z0-9_\-\.~/]+)){0,3}(?:\s+(?::[!]?)?[a-zA-Z0-9_\-\.\*\/]+){0,2}$'
|
||||
|
||||
if ($normalized_cmd | find -r $git_cmd_pattern | is-empty) {
|
||||
print $'(ansi r)Invalid git command format. (ansi g)Only simple `git show` or `git diff` commands are allowed.(ansi reset)'
|
||||
return false
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
# Setup scoop and install gawk for GitHub Windows runners
|
||||
# This command is essential for resolving the issue of simultaneously
|
||||
# applying include and exclude patterns on GitHub's Windows runners.
|
||||
def install-gawk-for-actions [] {
|
||||
# Install scoop using PowerShell
|
||||
pwsh -c r#'
|
||||
Invoke-Expression (New-Object System.Net.WebClient).DownloadString("https://get.scoop.sh")
|
||||
$env:Path = "$env:USERPROFILE\scoop\shims;" + $env:Path; scoop update; scoop install gawk
|
||||
'# | complete | get stdout | print
|
||||
let awk_bin = $'($nu.home-path)/scoop/shims/gawk.exe'
|
||||
let version = get-awk-ver $awk_bin
|
||||
{ awk_bin: $awk_bin, version: $version }
|
||||
# Coalesce the reasoning content
|
||||
def coalesce-reasoning [] {
|
||||
let msg = $in
|
||||
$msg.reasoning_content? | default $msg.reasoning?
|
||||
}
|
||||
|
||||
alias main = deepseek-review
|
||||
|
||||
138
nu/util.nu
Normal file
138
nu/util.nu
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env nu
|
||||
# Author: hustcer
|
||||
# Created: 2025/04/02 20:02:15
|
||||
#
|
||||
|
||||
use common.nu [ECODE, is-installed, compare-ver, windows?, mac?]
|
||||
|
||||
# AWK family version check for both awk and gawk
|
||||
# awk: awk version 20250116 -> 20250116
|
||||
# gawk: GNU Awk 5.3.1, API 4.0, (GNU MPFR 4.2.1, GNU MP 6.3.0) -> 5.3.1
|
||||
def get-awk-ver [awk_bin: string] {
|
||||
^$awk_bin --version | lines | first | split row , | first | split row ' ' | last
|
||||
}
|
||||
|
||||
# Prepare gawk for macOS
|
||||
export def prepare-awk [] {
|
||||
const MIN_GAWK_VERSION = '5.3.1'
|
||||
const MIN_AWK_VERSION = '20250116'
|
||||
let awk_installed = is-installed awk
|
||||
let gawk_installed = is-installed gawk
|
||||
|
||||
if $awk_installed {
|
||||
let awk_version = get-awk-ver awk
|
||||
if (compare-ver $awk_version $MIN_AWK_VERSION) >= 0 {
|
||||
print $'Current awk version: ($awk_version)'
|
||||
return 'awk'
|
||||
}
|
||||
}
|
||||
if $gawk_installed {
|
||||
let gawk_version = get-awk-ver gawk
|
||||
if (compare-ver $gawk_version $MIN_GAWK_VERSION) >= 0 {
|
||||
print $'Current gawk version: ($gawk_version)'
|
||||
return 'gawk'
|
||||
} else if (windows?) and ($env.GITHUB_ACTIONS? == 'true') {
|
||||
let awk_info = (install-gawk-for-actions)
|
||||
print $'Current gawk version: ($awk_info.version)'
|
||||
return $awk_info.awk_bin
|
||||
}
|
||||
}
|
||||
if (mac?) and (is-installed brew) {
|
||||
brew install gawk
|
||||
print $'Current gawk version: (get-awk-ver gawk)'
|
||||
return 'gawk'
|
||||
}
|
||||
if (not $awk_installed) and (not $gawk_installed) {
|
||||
print $'(ansi r)Neither `awk` nor `gawk` is installed, please install the latest version of `gawk`.(ansi reset)'
|
||||
exit $ECODE.MISSING_BINARY
|
||||
}
|
||||
print $'Current awk version: (get-awk-ver awk)'
|
||||
'awk'
|
||||
}
|
||||
|
||||
# Convert glob patterns to regex patterns
|
||||
# Pass in *.nu directly as a regular expression does not work, because * in
|
||||
# a regular expression needs to be attached to the previous pattern, the correct
|
||||
# form should be .* So we should convert each glob pattern to a regex pattern:
|
||||
# 1. Convert * to .*
|
||||
# 2. Convert ? to . (optional, as needed)
|
||||
# 3. Convert / to \/
|
||||
def glob-to-regex [patterns: list<string>] {
|
||||
# Handle empty patterns list
|
||||
if ($patterns | length) == 0 { return '' }
|
||||
|
||||
# Define a mapping of characters to escape
|
||||
let regex_escapes = {
|
||||
# Escape special regex characters first
|
||||
"\\.": "\\\\.",
|
||||
"\\+": "\\\\+",
|
||||
"\\^": "\\\\^",
|
||||
"\\$": "\\\\$",
|
||||
"\\(": "\\\\(",
|
||||
"\\)": "\\\\)",
|
||||
"\\[": "\\\\[",
|
||||
"\\]": "\\\\]",
|
||||
"\\{": "\\\\{",
|
||||
"\\}": "\\\\}",
|
||||
"\\|": "\\\\|",
|
||||
# Then convert glob patterns to regex patterns
|
||||
"*": ".*",
|
||||
"?": ".",
|
||||
"/": "\\/",
|
||||
}
|
||||
|
||||
$patterns
|
||||
| each { |pat|
|
||||
$regex_escapes | columns | reduce -f $pat { |k, acc|
|
||||
$acc | str replace -a $k ($regex_escapes | get $k)
|
||||
}
|
||||
}
|
||||
| str join '|'
|
||||
}
|
||||
|
||||
# Generate the awk include regex pattern string for the specified patterns
|
||||
export def generate-include-regex [patterns: list<string>] {
|
||||
let pattern = glob-to-regex $patterns
|
||||
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}p"
|
||||
}
|
||||
|
||||
# Generate the awk exclude regex pattern string for the specified patterns
|
||||
export def generate-exclude-regex [patterns: list<string>] {
|
||||
let pattern = glob-to-regex $patterns
|
||||
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}!p"
|
||||
}
|
||||
|
||||
# Check if the git command is safe to run in the shell
|
||||
# Validate command examples:
|
||||
# - git show
|
||||
# - git diff
|
||||
# - git show head~1
|
||||
# - git diff 2393375 71f5a31
|
||||
# - git diff 2393375 71f5a31 nu/*
|
||||
# - git diff 2393375 71f5a31 :!nu/*
|
||||
export def is-safe-git [cmd: string] {
|
||||
let normalized_cmd = ($cmd | str trim | str downcase)
|
||||
|
||||
# Define allowed command patterns with named capture groups for better validation
|
||||
let git_cmd_pattern = '^git\s+(show|diff)(?:\s+(?:[a-zA-Z0-9_\-\.~/]+(?::[a-zA-Z0-9_\-\.\*\/]+)?)){0,3}(?:\s+(?::[!]?)?[a-zA-Z0-9_\-\.\*\/]+){0,2}$'
|
||||
|
||||
if ($normalized_cmd | find -r $git_cmd_pattern | is-empty) {
|
||||
print $'(ansi r)Invalid git command format. (ansi g)Only simple `git show` or `git diff` commands are allowed.(ansi reset)'
|
||||
return false
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
# Setup scoop and install gawk for GitHub Windows runners
|
||||
# This command is essential for resolving the issue of simultaneously
|
||||
# applying include and exclude patterns on GitHub's Windows runners.
|
||||
def install-gawk-for-actions [] {
|
||||
# Install scoop using PowerShell
|
||||
pwsh -c r#'
|
||||
Invoke-Expression (New-Object System.Net.WebClient).DownloadString("https://get.scoop.sh")
|
||||
$env:Path = "$env:USERPROFILE\scoop\shims;" + $env:Path; scoop update; scoop install gawk
|
||||
'# | complete | get stdout | print
|
||||
let awk_bin = $'($nu.home-dir)/scoop/shims/gawk.exe'
|
||||
let version = get-awk-ver $awk_bin
|
||||
{ awk_bin: $awk_bin, version: $version }
|
||||
}
|
||||
@@ -1,39 +1,40 @@
|
||||
|
||||
use std/assert
|
||||
use std/testing *
|
||||
|
||||
use ../nu/common.nu [
|
||||
compare-ver, 'from env', is-installed, has-ref,
|
||||
git-check, compact-record, is-repo, windows?, mac?,
|
||||
]
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'compare-ver:v1.0.0 is greater than v0.999.0' [] {
|
||||
assert equal (compare-ver 1.0.0 0.999.0) 1
|
||||
assert equal (compare-ver v1.0.0 v0.999.0) 1
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'compare-ver:v1.0.1 is equal to v1.0.1' [] {
|
||||
assert equal (compare-ver 1.0.1 1.0.1) 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'compare-ver:v1.0.0 is equal to v1' [] {
|
||||
assert equal (compare-ver v1.0.0 v1) 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'compare-ver:v1.0.1 is greater than v1' [] {
|
||||
assert equal (compare-ver v1.0.1 v1) 1
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'compare-ver:v1.0.1 is lower than v1.1.0' [] {
|
||||
assert less (compare-ver 1.0.1 v1.1) 0
|
||||
assert equal (compare-ver 1.0.1 1.1.0) (-1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'from-env:.env load should work' [] {
|
||||
open tests/resources/.env.test | from env | load-env
|
||||
assert equal $env.CHAT_MODEL deepseek-chat
|
||||
@@ -43,35 +44,35 @@ def 'from-env:.env load should work' [] {
|
||||
assert equal $env.USER_PROMPT 'Please review the following code changes'
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'is-installed:binary install check should work' [] {
|
||||
assert equal (is-installed git) true
|
||||
assert equal (is-installed abc) false
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'has-ref:git repo should has HEAD ref' [] {
|
||||
assert equal (has-ref HEAD) true
|
||||
assert equal (has-ref 0000) false
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'is-repo:current dir is a git repo' [] {
|
||||
assert equal (is-repo) true
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'git-check:current dir is a git repo' [] {
|
||||
assert equal (git-check (pwd) --check-repo=1) true
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'compact-record:should work as expected' [] {
|
||||
assert equal ({a: null, b: '', c: 'abc' } | compact-record) { c: 'abc' }
|
||||
assert equal ({a: null, b: 0, c: 1, e: { f: 'g' } } | compact-record) { b: 0, c: 1, e: { f: 'g' } }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'OS check should work as expected' [] {
|
||||
# `$env.RUNNER_OS` Possible values are Linux, Windows, or macOS in GitHub Actions
|
||||
match $nu.os-info.name {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
|
||||
use std/assert
|
||||
|
||||
use ../nu/review.nu [
|
||||
get-diff, is-safe-git, prepare-awk,
|
||||
generate-include-regex, generate-exclude-regex,
|
||||
]
|
||||
use std/testing *
|
||||
use ../nu/diff.nu [get-diff]
|
||||
use ../nu/util.nu [is-safe-git, prepare-awk, generate-include-regex, generate-exclude-regex]
|
||||
|
||||
# Get the unicode width of the input string
|
||||
def get-uw [] { $in | str stats | get unicode-width }
|
||||
|
||||
#[before-all]
|
||||
@before-all
|
||||
def setup [] {
|
||||
let awk_bin = (prepare-awk)
|
||||
let patch = open -r tests/resources/diff.patch
|
||||
@@ -17,7 +15,7 @@ def setup [] {
|
||||
{ patch: $patch, awk: $awk_bin, SHA: 22e7b71 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'is-safe-git:should work as expected' [] {
|
||||
assert equal (is-safe-git 'git diff') true
|
||||
assert equal (is-safe-git 'git show') true
|
||||
@@ -44,9 +42,11 @@ def 'is-safe-git:should work as expected' [] {
|
||||
assert equal (is-safe-git 'git diff f536acc 0dd0eb5 :!nu/* >> out.txt') false
|
||||
assert equal (is-safe-git 'git diff f536acc 0dd0eb5 :!nu/* < in.txt') false
|
||||
assert equal (is-safe-git 'git diff f536acc 0dd0eb5 :!nu/* << in.txt') false
|
||||
assert equal (is-safe-git 'git show head:nu/common.nu') true
|
||||
assert equal (is-safe-git 'git show HEAD:nu/common.nu') true
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'generate-include-regex:should work as expected' [] {
|
||||
let patch = $in.patch
|
||||
let awk_bin = $in.awk
|
||||
@@ -56,7 +56,7 @@ def 'generate-include-regex:should work as expected' [] {
|
||||
assert equal ($patch | ^$awk_bin (generate-include-regex [.env*, *.md, nu/*]) | get-uw) 6871
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'generate-exclude-regex:should work as expected' [] {
|
||||
let patch = $in.patch
|
||||
let awk_bin = $in.awk
|
||||
@@ -64,7 +64,7 @@ def 'generate-exclude-regex:should work as expected' [] {
|
||||
assert equal ($patch | ^$awk_bin (generate-exclude-regex [.env*, *.md, nu/*]) | get-uw) (1350 + 99)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'both include and exclude should work as expected' [] {
|
||||
let patch = $in.patch
|
||||
let awk_bin = $in.awk
|
||||
@@ -74,7 +74,7 @@ def 'both include and exclude should work as expected' [] {
|
||||
| get-uw) 2576
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'both exclude and include should work as expected' [] {
|
||||
let patch = $in.patch
|
||||
let awk_bin = $in.awk
|
||||
@@ -84,7 +84,7 @@ def 'both exclude and include should work as expected' [] {
|
||||
| get-uw) 2576
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'get-diff:get patch from remote PR should work' [] {
|
||||
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
||||
const repo = 'hustcer/deepseek-review'
|
||||
@@ -94,7 +94,7 @@ def 'get-diff:get patch from remote PR should work' [] {
|
||||
| str join "\n" | get-uw) 7923
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'get-diff:get patch from remote PR with include should work' [] {
|
||||
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
||||
const repo = 'hustcer/deepseek-review'
|
||||
@@ -103,7 +103,7 @@ def 'get-diff:get patch from remote PR with include should work' [] {
|
||||
assert equal ($patch | get-uw) 2576
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'get-diff:get patch from remote PR with exclude should work' [] {
|
||||
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
||||
const repo = 'hustcer/deepseek-review'
|
||||
@@ -112,7 +112,7 @@ def 'get-diff:get patch from remote PR with exclude should work' [] {
|
||||
assert equal ($patch | get-uw) 555
|
||||
}
|
||||
|
||||
#[test]
|
||||
@test
|
||||
def 'get-diff:get patch from remote PR with exclude & include should work' [] {
|
||||
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
||||
const repo = 'hustcer/deepseek-review'
|
||||
|
||||
Reference in New Issue
Block a user