mirror of
https://github.com/hustcer/deepseek-review.git
synced 2026-05-13 05:16:05 +08:00
Compare commits
36 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 |
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:
|
As a senior Nushell engineer, perform comprehensive script review with focus on:
|
||||||
|
|
||||||
### 1. Core Requirements:
|
### 1. Core Requirements:
|
||||||
- Validate Nu 0.90+ compatibility
|
- Validate Nu 0.100+ compatibility
|
||||||
- Check structured data handling
|
- Check structured data handling
|
||||||
- Verify pipeline efficiency
|
- Verify pipeline efficiency
|
||||||
- Assess module organization
|
- Assess module organization
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
- Parallel execution opportunities
|
- Parallel execution opportunities
|
||||||
|
|
||||||
**Rules:**
|
**Rules:**
|
||||||
- Target Nu 0.90+ features
|
- Target Nu 0.100+ features
|
||||||
- Highlight data flow vulnerabilities
|
- Highlight data flow vulnerabilities
|
||||||
- Suggest structured data optimizations
|
- Suggest structured data optimizations
|
||||||
- Keep feedback Nu-specific
|
- Keep feedback Nu-specific
|
||||||
|
|||||||
7
.github/workflows/tests.yml
vendored
7
.github/workflows/tests.yml
vendored
@@ -15,6 +15,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- develop
|
- develop
|
||||||
|
- feature/test
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
@@ -41,12 +42,12 @@ jobs:
|
|||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Checkout Nutest Repo
|
- name: Checkout Nutest Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: v1.0.1
|
ref: main
|
||||||
path: nutest
|
path: nutest
|
||||||
repository: vyadh/nutest
|
repository: vyadh/nutest
|
||||||
sparse-checkout: nutest/
|
sparse-checkout: nutest/
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
.env
|
.env
|
||||||
.env.dev
|
.env.dev
|
||||||
.env.local
|
.env.local
|
||||||
|
review.md
|
||||||
config.yml
|
config.yml
|
||||||
prompts.yaml
|
prompts.yaml
|
||||||
|
|||||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -1,6 +1,73 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [1.16.0] - 2025-04-05
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
@@ -13,9 +80,9 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### Miscellaneous Tasks
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
- Add alias guide for `fish`
|
- Add alias setup guide for `fish`
|
||||||
- Add openrouter.ai config example
|
- Add openrouter.ai config example
|
||||||
- Set minimum required nushell version to v0.103
|
- Set minimum required `nushell` version to v0.103
|
||||||
|
|
||||||
### Refactor
|
### Refactor
|
||||||
|
|
||||||
|
|||||||
2
Justfile
2
Justfile
@@ -28,7 +28,7 @@ alias cr := code-review
|
|||||||
|
|
||||||
# Used to handle the path separator issue
|
# Used to handle the path separator issue
|
||||||
DEEPSEEK_REVIEW_PATH := parent_directory(justfile())
|
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' }
|
_query_plugin := if os_family() == 'windows' { 'nu_plugin_query.exe' } else { 'nu_plugin_query' }
|
||||||
|
|
||||||
# To pass arguments to a dependency, put the dependency
|
# To pass arguments to a dependency, put the dependency
|
||||||
|
|||||||
98
README.md
98
README.md
@@ -7,30 +7,29 @@
|
|||||||
|
|
||||||
[中文说明](README.zh-CN.md)
|
[中文说明](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
|
## Features
|
||||||
|
|
||||||
### GitHub Action
|
### GitHub Action
|
||||||
|
|
||||||
- Automate PR Reviews with DeepSeek via 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`.
|
- Cross-platform Support: Compatible with GitHub Runners across `macOS`, `Ubuntu`, and `Windows`.
|
||||||
|
|
||||||
### Local Code Review
|
### Local Code Review
|
||||||
|
|
||||||
- Streaming Output Support for Local Code Review
|
- Streaming output support for local code review
|
||||||
- Review Remote GitHub PRs Directly from Your Local CLI
|
- Review remote GitHub PRs directly from your local CLI
|
||||||
- Review Commit Changes with DeepSeek for Any Local Repository by CLI
|
- Review commit changes with DeepSeek for any local repository via CLI
|
||||||
- Support On-demand Changes Generation via `git show`/`git diff` Command for Further Code Review
|
- Support on-demand change generation via `git show`/`git diff` commands for further code review
|
||||||
- Cross-platform Compatibility: Designed to function seamlessly across all platforms capable of running [Nushell](https://github.com/nushell/nushell)
|
- 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
|
### Both GH Action & Local
|
||||||
|
|
||||||
- Support Both DeepSeek's `V3` & `R1` Models
|
- Support both DeepSeek's `V3` and `R1` models
|
||||||
- Fully Customizable: Choose Models, Base URLs, and Prompts
|
- Fully customizable: Choose models, base URLs, and prompts
|
||||||
- Supports Self-Hosted DeepSeek Models for Enhanced Flexibility
|
- Support self-hosted DeepSeek models for enhanced flexibility
|
||||||
- Perform Code Reviews for Changes That either Include or Exclude Specific Files
|
- Perform code reviews for changes that either include or exclude specific files
|
||||||
|
|
||||||
## Planned Features
|
## Planned Features
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@
|
|||||||
|
|
||||||
## Code Review with GitHub Action
|
## 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:
|
Add a GitHub workflow with the following contents:
|
||||||
|
|
||||||
@@ -81,13 +80,13 @@ jobs:
|
|||||||
|
|
||||||
</details>
|
</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 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)
|
- [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
|
```yaml
|
||||||
name: Code Review
|
name: Code Review
|
||||||
@@ -120,21 +119,21 @@ With this setup, DeepSeek code review will not run automatically upon PR creatio
|
|||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| -------------- | ------ | ----------------------------------------------------------------------- |
|
| -------------- | ------ | ----------------------------------------------------------------------- |
|
||||||
| chat-token | String | Required, DeepSeek API Token |
|
| 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` |
|
| 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 |
|
| 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 |
|
| 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` |
|
| 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, The comma separated file patterns to include in the code review. No default |
|
| include-patterns | String | Optional, 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` |
|
| 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 }}`. |
|
| github-token | String | Optional, The `GITHUB_TOKEN` secret or personal access token to authenticate. Defaults to `${{ github.token }}`. |
|
||||||
|
|
||||||
**DeepSeek API Call Payload**:
|
**DeepSeek API Call Payload**:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
// `$model` default value: deepseek-chat
|
// `$model` default value: deepseek-v4-flash
|
||||||
model: $model,
|
model: $model,
|
||||||
stream: false,
|
stream: false,
|
||||||
temperature: $temperature,
|
temperature: $temperature,
|
||||||
@@ -153,18 +152,18 @@ With this setup, DeepSeek code review will not run automatically upon PR creatio
|
|||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
>
|
>
|
||||||
> You can control the language of the code review results by the language of the
|
> 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 default prompt language is currently English. When you use a Chinese
|
||||||
> Prompt, the generated code review results will be in Chinese.
|
> prompt, the generated code review results will be in Chinese.
|
||||||
|
|
||||||
## Local Code Review
|
## Local Code Review
|
||||||
|
|
||||||
### Required Tools
|
### 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(min version required: `0.103`).
|
- [`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 the preferred choice.
|
- 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:
|
- 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
|
```console
|
||||||
@@ -178,19 +177,20 @@ Flags:
|
|||||||
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review
|
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review
|
||||||
-n, --pr-number <string>: GitHub PR number
|
-n, --pr-number <string>: GitHub PR number
|
||||||
-k, --gh-token <string>: Your GitHub token, fallback to GITHUB_TOKEN env var
|
-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>: Git diff starting commit SHA
|
||||||
-f, --diff-from <string>: Diff from git REF
|
-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
|
-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.
|
-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
|
-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
|
-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,
|
-s, --sys-prompt <string>: Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||||
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||||
-i, --include <string>: Comma separated file patterns to include in the code review
|
-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
|
-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`
|
-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
|
-h, --help: Display the help message for this command
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
@@ -200,27 +200,27 @@ Parameters:
|
|||||||
|
|
||||||
### Environment Configuration
|
### 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]
|
> [!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
|
```sh
|
||||||
# For Nushell: Modify config.nu and add:
|
# For Nushell: Modify config.nu and add:
|
||||||
alias cr = nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml
|
alias cr = nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml
|
||||||
|
|
||||||
# Modify ~/.zshrc for zsh or ~/.bashrc for bash or ~/.config/fish/config.fish for fish 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"
|
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 please set cr alias by editing $PROFILE and add:
|
# For Windows PowerShell users, set the cr alias by editing $PROFILE and add:
|
||||||
function cr {
|
function cr {
|
||||||
nu D:\absolute\path\to\deepseek-review\cr --config D:\absolute\path\to\deepseek-review\config.yml @args
|
nu D:\absolute\path\to\deepseek-review\cr --config D:\absolute\path\to\deepseek-review\config.yml @args
|
||||||
}
|
}
|
||||||
@@ -233,18 +233,20 @@ function cr {
|
|||||||
To review a local repository:
|
To review a local repository:
|
||||||
|
|
||||||
- Navigate to the Git repository directory.
|
- 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**
|
**Usage Examples**
|
||||||
|
|
||||||
```sh
|
```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
|
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
|
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
|
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 --patch-cmd 'git diff head~3'
|
||||||
cr -c 'git show head~3'
|
cr -c 'git show head~3'
|
||||||
cr -c 'git diff 2393375 71f5a31'
|
cr -c 'git diff 2393375 71f5a31'
|
||||||
@@ -257,17 +259,17 @@ cr -c 'git diff 2393375 71f5a31 :!nu/*'
|
|||||||
|
|
||||||
When reviewing a remote GitHub PR locally:
|
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`.
|
- 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**
|
**Usage Examples**
|
||||||
|
|
||||||
```sh
|
```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
|
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
|
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
|
cr --pr-number 31 --exclude pnpm-lock.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
本工具也支持使用 SiliconCloud 上的 DeepSeek 模型,[注册](https://cloud.siliconflow.cn/i/rqCdIxzS) 就**免费赠送 2000 万 Token**,赶紧试试吧!
|
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
### GitHub Action
|
### GitHub Action
|
||||||
@@ -21,6 +19,7 @@
|
|||||||
- 通过本地 CLI 直接审查远程 GitHub PR
|
- 通过本地 CLI 直接审查远程 GitHub PR
|
||||||
- 通过本地 CLI 使用 DeepSeek 审查任何本地仓库的提交变更
|
- 通过本地 CLI 使用 DeepSeek 审查任何本地仓库的提交变更
|
||||||
- 允许通过自定义 `git show`/`git diff` 命令生成变更记录并进行审查
|
- 允许通过自定义 `git show`/`git diff` 命令生成变更记录并进行审查
|
||||||
|
- 允许将代码审查结果以 Markdown 格式输出到指定文件
|
||||||
- 跨平台:理论上只要能运行 [Nushell](https://github.com/nushell/nushell) 即可使用本工具
|
- 跨平台:理论上只要能运行 [Nushell](https://github.com/nushell/nushell) 即可使用本工具
|
||||||
|
|
||||||
### 本地或 GH Action
|
### 本地或 GH Action
|
||||||
@@ -118,12 +117,12 @@ jobs:
|
|||||||
| 名称 | 类型 | 描述 |
|
| 名称 | 类型 | 描述 |
|
||||||
| -------------- | ------ | -------------------------------------------------------------- |
|
| -------------- | ------ | -------------------------------------------------------------- |
|
||||||
| chat-token | String | 必填,DeepSeek API Token |
|
| 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` |
|
| base-url | String | 可选,DeepSeek API Base URL, 默认为 `https://api.deepseek.com` |
|
||||||
| max-length | Int | 可选,待审查内容的最大 Unicode 长度, 默认 `0` 表示没有限制,超过非零值则跳过审查 |
|
| max-length | Int | 可选,待审查内容的最大 Unicode 长度, 默认 `0` 表示没有限制,超过非零值则跳过审查 |
|
||||||
| sys-prompt | String | 可选,系统提示词对应入参中的 `$sys_prompt`, 默认值见后文注释 |
|
| sys-prompt | String | 可选,系统提示词对应入参中的 `$sys_prompt`, 默认值见后文注释 |
|
||||||
| user-prompt | String | 可选,用户提示词对应入参中的 `$user_prompt`, 默认值见后文注释 |
|
| user-prompt | String | 可选,用户提示词对应入参中的 `$user_prompt`, 默认值见后文注释 |
|
||||||
| temperature | Number | 可选,采样温度,介于 `0` 和 `2` 之间, 默认值 `1.0` |
|
| temperature | Number | 可选,采样温度,介于 `0` 和 `2` 之间, 默认值 `0.3` |
|
||||||
| include-patterns | String | 可选,代码审查中要包含的以逗号分隔的文件模式,无默认值 |
|
| include-patterns | String | 可选,代码审查中要包含的以逗号分隔的文件模式,无默认值 |
|
||||||
| exclude-patterns | String | 可选,代码审查中要排除的以逗号分隔的文件模式,默认值为 `pnpm-lock.yaml,package-lock.json,*.lock` |
|
| exclude-patterns | String | 可选,代码审查中要排除的以逗号分隔的文件模式,默认值为 `pnpm-lock.yaml,package-lock.json,*.lock` |
|
||||||
| github-token | String | 可选,用于访问 API 进行 PR 管理的 GitHub Token,默认为 `${{ github.token }}` |
|
| github-token | String | 可选,用于访问 API 进行 PR 管理的 GitHub Token,默认为 `${{ github.token }}` |
|
||||||
@@ -132,7 +131,7 @@ DeepSeek 接口调用入参:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
// `$model` default value: deepseek-chat
|
// `$model` default value: deepseek-v4-flash
|
||||||
model: $model,
|
model: $model,
|
||||||
stream: false,
|
stream: false,
|
||||||
temperature: $temperature,
|
temperature: $temperature,
|
||||||
@@ -160,7 +159,7 @@ DeepSeek 接口调用入参:
|
|||||||
|
|
||||||
在本地进行代码审查,支持 `macOS`, `Ubuntu` & `Windows` 不过需要安装以下工具:
|
在本地进行代码审查,支持 `macOS`, `Ubuntu` & `Windows` 不过需要安装以下工具:
|
||||||
|
|
||||||
- [`Nushell`](https://www.nushell.sh/book/installation.html), 建议安装最新版本(最低版本 `0.103`)
|
- [`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`
|
- [`awk`](https://github.com/onetrueawk/awk) 或者 [`gawk`](https://www.gnu.org/software/gawk/) 的最新版版本,优先推荐 `gawk`
|
||||||
- 接下来只需要把本仓库代码克隆到本地,然后进入仓库目录执行 `nu cr -h` 即可看到类似如下输出:
|
- 接下来只需要把本仓库代码克隆到本地,然后进入仓库目录执行 `nu cr -h` 即可看到类似如下输出:
|
||||||
|
|
||||||
@@ -175,19 +174,20 @@ Flags:
|
|||||||
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review
|
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review
|
||||||
-n, --pr-number <string>: GitHub PR number
|
-n, --pr-number <string>: GitHub PR number
|
||||||
-k, --gh-token <string>: Your GitHub token, fallback to GITHUB_TOKEN env var
|
-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>: Git diff starting commit SHA
|
||||||
-f, --diff-from <string>: Diff from git REF
|
-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
|
-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.
|
-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
|
-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
|
-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,
|
-s, --sys-prompt <string>: Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||||
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||||
-i, --include <string>: Comma separated file patterns to include in the code review
|
-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
|
-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`
|
-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
|
-h, --help: Display the help message for this command
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
@@ -238,6 +238,8 @@ function cr {
|
|||||||
cr
|
cr
|
||||||
# 对本地当前目录所在仓库 `git diff f536acc` 修改内容进行代码审查
|
# 对本地当前目录所在仓库 `git diff f536acc` 修改内容进行代码审查
|
||||||
cr --diff-from f536acc
|
cr --diff-from f536acc
|
||||||
|
# 对本地当前目录所在仓库 `git diff f536acc` 修改内容进行代码审查并将审查结果输出到 review.md
|
||||||
|
cr --diff-from f536acc --output review.md
|
||||||
# 对本地当前目录所在仓库 `git diff f536acc 0dd0eb5` 修改内容进行代码审查
|
# 对本地当前目录所在仓库 `git diff f536acc 0dd0eb5` 修改内容进行代码审查
|
||||||
cr --diff-from f536acc --diff-to 0dd0eb5
|
cr --diff-from f536acc --diff-to 0dd0eb5
|
||||||
# 通过 --patch-cmd 参数对本地当前目录所在仓库变更内容进行审查
|
# 通过 --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.'
|
description: 'The maximum length of the content for review, 0 means no limit.'
|
||||||
model:
|
model:
|
||||||
required: false
|
required: false
|
||||||
default: 'deepseek-chat'
|
default: 'deepseek-v4-flash'
|
||||||
description: 'The DeepSeek model to choose for code review.'
|
description: 'The DeepSeek model to choose for code review.'
|
||||||
temperature:
|
temperature:
|
||||||
required: false
|
required: false
|
||||||
default: 1.0
|
default: 0.3
|
||||||
description: 'The temperature of the model.'
|
description: 'The temperature of the model.'
|
||||||
base-url:
|
base-url:
|
||||||
required: false
|
required: false
|
||||||
@@ -58,9 +58,9 @@ runs:
|
|||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Nu
|
- name: Setup Nu
|
||||||
uses: hustcer/setup-nu@v3
|
uses: hustcer/setup-nu@v3.23
|
||||||
with:
|
with:
|
||||||
version: 0.103.0
|
version: 0.112.2
|
||||||
|
|
||||||
- name: DeepSeek Code Review
|
- name: DeepSeek Code Review
|
||||||
shell: nu {0}
|
shell: nu {0}
|
||||||
@@ -78,7 +78,7 @@ runs:
|
|||||||
let includePatterns = '${{ inputs.include-patterns }}'
|
let includePatterns = '${{ inputs.include-patterns }}'
|
||||||
let excludePatterns = '${{ inputs.exclude-patterns }}'
|
let excludePatterns = '${{ inputs.exclude-patterns }}'
|
||||||
let maxLength = try { '${{ inputs.max-length }}' | into int } catch { 0 }
|
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
|
(deepseek-review $token
|
||||||
--model $model
|
--model $model
|
||||||
--repo $repo
|
--repo $repo
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ settings:
|
|||||||
# If the content length exceeds the non-zero limit, the review will be skipped
|
# If the content length exceeds the non-zero limit, the review will be skipped
|
||||||
# Note that it's unicode width not LLM token length
|
# Note that it's unicode width not LLM token length
|
||||||
max-length: 0
|
max-length: 0
|
||||||
# The temperature of the model, The value should be between 0 and 2, with default value 1.0
|
# The temperature of the model, The value should be between 0 and 2, with default value 0.3
|
||||||
temperature: 1.0
|
temperature: 0.3
|
||||||
# The user prompt name to use for DeepSeek API select from 'prompts.user'
|
# The user prompt name to use for DeepSeek API select from 'prompts.user'
|
||||||
user-prompt: 'default'
|
user-prompt: 'default'
|
||||||
# The system prompt name to use for DeepSeek API select from 'prompts.system'
|
# The system prompt name to use for DeepSeek API select from 'prompts.system'
|
||||||
@@ -50,7 +50,7 @@ providers:
|
|||||||
token: 'YOUR_DEEPSEEK_TOKEN' # Required, The API token for the provider
|
token: 'YOUR_DEEPSEEK_TOKEN' # Required, The API token for the provider
|
||||||
base-url: 'https://api.deepseek.com'
|
base-url: 'https://api.deepseek.com'
|
||||||
models:
|
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
|
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
|
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
|
description: 'DeepSeek V3' # Optional, Description of the model, won't be used actually
|
||||||
|
|||||||
16
cr
16
cr
@@ -4,8 +4,8 @@
|
|||||||
# Description: A wrapper for nu/review.nu as the main entry point of the project.
|
# Description: A wrapper for nu/review.nu as the main entry point of the project.
|
||||||
|
|
||||||
use nu/config.nu *
|
use nu/config.nu *
|
||||||
use nu/common.nu [hr-line, check-nushell, ECODE]
|
|
||||||
use nu/review.nu [deepseek-review]
|
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
|
# Use DeepSeek AI to review code changes locally or in GitHub Actions
|
||||||
def main [
|
def main [
|
||||||
@@ -14,19 +14,20 @@ def main [
|
|||||||
--repo(-r): string, # GitHub repo name, e.g. hustcer/deepseek-review
|
--repo(-r): string, # GitHub repo name, e.g. hustcer/deepseek-review
|
||||||
--pr-number(-n): string, # GitHub PR number
|
--pr-number(-n): string, # GitHub PR number
|
||||||
--gh-token(-k): string, # Your GitHub token, fallback to GITHUB_TOKEN env var
|
--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, # Git diff starting commit SHA
|
||||||
--diff-from(-f): string, # Diff from git REF
|
--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
|
--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.
|
--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
|
--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
|
--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,
|
--sys-prompt(-s): string # Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||||
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||||
--include(-i): string, # Comma separated file patterns to include in the code review
|
--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
|
--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`
|
--config(-C): string # Config file path, default to `config.yml`
|
||||||
|
--output(-o): string, # Output file path
|
||||||
] {
|
] {
|
||||||
|
|
||||||
check-nushell
|
check-nushell
|
||||||
@@ -36,8 +37,7 @@ def main [
|
|||||||
deepseek-review $token
|
deepseek-review $token
|
||||||
--repo=$repo
|
--repo=$repo
|
||||||
--debug=$debug
|
--debug=$debug
|
||||||
--include=$include
|
--output=$output
|
||||||
--exclude=$exclude
|
|
||||||
--model=$env.CHAT_MODEL
|
--model=$env.CHAT_MODEL
|
||||||
--base-url=$base_url
|
--base-url=$base_url
|
||||||
--chat-url=$chat_url
|
--chat-url=$chat_url
|
||||||
@@ -50,5 +50,7 @@ def main [
|
|||||||
--sys-prompt=$sys_prompt
|
--sys-prompt=$sys_prompt
|
||||||
--user-prompt=$user_prompt
|
--user-prompt=$user_prompt
|
||||||
--temperature=$temperature
|
--temperature=$temperature
|
||||||
|
--include=($include | default $env.INCLUDE_PATTERNS?)
|
||||||
|
--exclude=($exclude | default $env.EXCLUDE_PATTERNS?)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ words:
|
|||||||
- hustcer
|
- hustcer
|
||||||
- Nushell
|
- Nushell
|
||||||
- creatio
|
- creatio
|
||||||
|
- pipefail
|
||||||
- justfile
|
- justfile
|
||||||
- lefthook
|
- lefthook
|
||||||
- deepseek
|
- deepseek
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "deepseek-review",
|
"name": "deepseek-review",
|
||||||
"version": "1.16.0",
|
"version": "1.20.0",
|
||||||
"actionVer": "v1.16",
|
"actionVer": "v1.20",
|
||||||
"author": "hustcer",
|
"author": "hustcer",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"github": "https://github.com/hustcer/deepseek-review",
|
"github": "https://github.com/hustcer/deepseek-review",
|
||||||
|
|||||||
90
nu/common.nu
90
nu/common.nu
@@ -51,8 +51,8 @@ export def compare-ver [v1: string, v2: string] {
|
|||||||
# If you want to compare more parts use the following code:
|
# 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 ($a | length) ($b | length)] | math max)
|
||||||
for i in 0..2 {
|
for i in 0..2 {
|
||||||
let x = $a | get -i $i | default 0
|
let x = $a | get -o $i | default 0
|
||||||
let y = $b | get -i $i | default 0
|
let y = $b | get -o $i | default 0
|
||||||
if $x > $y { return 1 }
|
if $x > $y { return 1 }
|
||||||
if $x < $y { return (-1) }
|
if $x < $y { return (-1) }
|
||||||
}
|
}
|
||||||
@@ -81,28 +81,69 @@ export def check-nushell [--debug] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Converts a .env file into a record
|
# Converts a .env file into a record
|
||||||
# may be used like this: open .env | load-env
|
# May be used like this: open .env | load-env
|
||||||
# works with quoted and unquoted .env files
|
# Works with quoted and unquoted .env files
|
||||||
export def 'from env' []: string -> record {
|
export def "from env" []: string -> record {
|
||||||
lines
|
let input = $in
|
||||||
| split column '#' # remove comments
|
|
||||||
| get column1
|
# Process escape sequences in double-quoted values using str replace chain
|
||||||
| parse '{key}={value}'
|
# Use NUL char as placeholder to avoid replacement conflicts
|
||||||
| update value {
|
let process_escapes = {|content: string|
|
||||||
str trim # Trim whitespace between value and inline comments
|
$content
|
||||||
| str trim -c '"' # unquote double-quoted values
|
| str replace -a '\\' (char nul) # Placeholder for \\ to avoid conflicts
|
||||||
| str trim -c "'" # unquote single-quoted values
|
| str replace -a '\n' (char nl)
|
||||||
| str replace -a "\\n" "\n" # replace `\n` with newline char
|
| str replace -a '\r' (char cr)
|
||||||
| str replace -a "\\r" "\r" # replace `\r` with carriage return
|
| str replace -a '\t' (char tab)
|
||||||
| str replace -a "\\t" "\t" # replace `\t` with tab
|
| str replace -a '\"' '"'
|
||||||
}
|
| str replace -a (char nul) '\' # Restore \\ to single \
|
||||||
| transpose -r -d
|
}
|
||||||
|
|
||||||
|
# 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
|
# Compact the record by removing empty columns
|
||||||
export def compact-record []: record -> record {
|
export def compact-record []: record -> record {
|
||||||
let record = $in
|
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
|
$record | reject ...$empties
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +193,8 @@ export def git-check [
|
|||||||
# Check if current directory is a git repo
|
# Check if current directory is a git repo
|
||||||
export def is-repo [] {
|
export def is-repo [] {
|
||||||
let checkRepo = try {
|
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 {
|
} catch {
|
||||||
({ stdout: 'false' })
|
({ stdout: 'false' })
|
||||||
}
|
}
|
||||||
@@ -164,13 +206,13 @@ export def has-ref [
|
|||||||
ref: string # The git ref to check
|
ref: string # The git ref to check
|
||||||
] {
|
] {
|
||||||
if not (is-repo) { return false }
|
if not (is-repo) { return false }
|
||||||
# Brackets were required here, or error will occur
|
# Put `complete` inside `do` block to avoid pipefail error in Nushell 0.110+
|
||||||
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 }
|
if ($parse.stdout | is-empty) { false } else { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
# Notify the user that the `CHAT_TOKEN` hasn't been configured
|
# Notify the user that the `CHAT_TOKEN` hasn't been configured
|
||||||
export const NO_TOKEN_TIP = (
|
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" +
|
"**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" +
|
"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).")
|
"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
|
# Check if the specified type of prompt key exists in the config.yml file
|
||||||
def check-prompt [options: record, type: string] {
|
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) {
|
if ($prompt_key | is-empty) {
|
||||||
print $'(ansi r)The ($type) prompt key is missing in `settings.($type)-prompt` config.yml file.(ansi reset)'
|
print $'(ansi r)The ($type) prompt key is missing in `settings.($type)-prompt` config.yml file.(ansi reset)'
|
||||||
exit $ECODE.INVALID_PARAMETER
|
exit $ECODE.INVALID_PARAMETER
|
||||||
}
|
}
|
||||||
let prompt = $options.prompts | get -i $type
|
let prompt = $options.prompts | get -o $type
|
||||||
| default []
|
| default []
|
||||||
| where name == $prompt_key
|
| where name == $prompt_key
|
||||||
| get -i 0.prompt
|
| get -o 0.prompt
|
||||||
if ($prompt | is-empty) {
|
if ($prompt | is-empty) {
|
||||||
print $'The ($type) prompt (ansi r)($prompt_key)(ansi reset) is missing in `prompts.($type)` of config.yml file.'
|
print $'The ($type) prompt (ansi r)($prompt_key)(ansi reset) is missing in `prompts.($type)` of config.yml file.'
|
||||||
exit $ECODE.INVALID_PARAMETER
|
exit $ECODE.INVALID_PARAMETER
|
||||||
@@ -59,11 +59,11 @@ def check-providers [options: record] {
|
|||||||
exit $ECODE.INVALID_PARAMETER
|
exit $ECODE.INVALID_PARAMETER
|
||||||
}
|
}
|
||||||
# Each provider should have name, token and models field
|
# Each provider should have name, token and models field
|
||||||
$options.providers | each {|it|
|
$options.providers | each {|p|
|
||||||
let empties = [name token models] | filter { |field| $it | get -i $field | is-empty }
|
let empties = [name token models] | where { |field| $p | get -o $field | is-empty }
|
||||||
if ($empties | is-not-empty) {
|
if ($empties | is-not-empty) {
|
||||||
print $'Field (ansi r)`($empties | str join ,)`(ansi reset) should not be empty for provider:'
|
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
|
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
|
# 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
|
file-exists $config
|
||||||
let options = open $config
|
let options = open $config
|
||||||
check-prompts $options
|
check-prompts $options
|
||||||
@@ -105,7 +106,7 @@ def get-model-envs [settings: record, model?: string = ''] {
|
|||||||
let provider = $settings.providers
|
let provider = $settings.providers
|
||||||
| default []
|
| default []
|
||||||
| where name == $name
|
| where name == $name
|
||||||
| get -i 0
|
| get -o 0
|
||||||
| default {}
|
| default {}
|
||||||
let model_name = $provider.models
|
let model_name = $provider.models
|
||||||
| default []
|
| default []
|
||||||
@@ -114,7 +115,7 @@ def get-model-envs [settings: record, model?: string = ''] {
|
|||||||
} else {
|
} else {
|
||||||
$it.name == $model or $it.alias? == $model }
|
$it.name == $model or $it.alias? == $model }
|
||||||
}
|
}
|
||||||
| get -i 0.name
|
| get -o 0.name
|
||||||
| default $model
|
| default $model
|
||||||
|
|
||||||
{ CHAT_TOKEN: $provider.token?, BASE_URL: $provider.base-url?, CHAT_URL: $provider.chat-url?, CHAT_MODEL: $model_name }
|
{ 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?
|
let user_prompt = $all_settings.prompts?.user?
|
||||||
| default []
|
| default []
|
||||||
| where name == ($settings.user-prompt? | default '')
|
| where name == ($settings.user-prompt? | default '')
|
||||||
| get -i 0.prompt
|
| get -o 0.prompt
|
||||||
|
|
||||||
let system_prompt = $all_settings.prompts?.system?
|
let system_prompt = $all_settings.prompts?.system?
|
||||||
| default []
|
| default []
|
||||||
| where name == ($settings.system-prompt? | default '')
|
| where name == ($settings.system-prompt? | default '')
|
||||||
| get -i 0.prompt
|
| get -o 0.prompt
|
||||||
|
|
||||||
let model_envs = get-model-envs $all_settings $model
|
let model_envs = get-model-envs $all_settings $model
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# Description: Diff command for DeepSeek-Review
|
# Description: Diff command for DeepSeek-Review
|
||||||
|
|
||||||
use common.nu [GITHUB_API_BASE, ECODE, git-check, has-ref]
|
use common.nu [GITHUB_API_BASE, ECODE, git-check, has-ref]
|
||||||
use util.nu [generate-include-regex, generate-exclude-regex, prepare-awk]
|
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
|
# If the PR title or body contains any of these keywords, skip the review
|
||||||
const IGNORE_REVIEW_KEYWORDS = ['skip review' 'skip cr']
|
const IGNORE_REVIEW_KEYWORDS = ['skip review' 'skip cr']
|
||||||
@@ -80,6 +80,13 @@ def get-pr-diff [
|
|||||||
exit $ECODE.SUCCESS
|
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
|
# Get the diff content of the PR
|
||||||
http get -H $DIFF_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)' | str trim
|
http get -H $DIFF_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)' | str trim
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ export def 'make-release' [
|
|||||||
export def has-ref [
|
export def has-ref [
|
||||||
ref: string # The git ref to check
|
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 }
|
if not ($checkRepo.stdout =~ 'true') { return false }
|
||||||
# Brackets were required here, or error will occur
|
let parse = (do { git rev-parse --verify -q $ref | complete })
|
||||||
let parse = (do -i { git rev-parse --verify -q $ref } | complete)
|
|
||||||
if ($parse.stdout | is-empty) { false } else { true }
|
if ($parse.stdout | is-empty) { false } else { true }
|
||||||
}
|
}
|
||||||
|
|||||||
76
nu/review.nu
76
nu/review.nu
@@ -43,8 +43,8 @@ const IGNORED_MESSAGES = {
|
|||||||
const HTTP_HEADERS = [User-Agent curl/8.9]
|
const HTTP_HEADERS = [User-Agent curl/8.9]
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
MODEL: 'deepseek-chat',
|
MODEL: 'deepseek-v4-flash',
|
||||||
TEMPERATURE: 1.0,
|
TEMPERATURE: 0.3,
|
||||||
BASE_URL: 'https://api.deepseek.com',
|
BASE_URL: 'https://api.deepseek.com',
|
||||||
USER_PROMPT: 'Please review the following code changes:',
|
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.',
|
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.',
|
||||||
@@ -55,34 +55,39 @@ export def --env deepseek-review [
|
|||||||
token?: string, # Your DeepSeek API token, fallback to CHAT_TOKEN env var
|
token?: string, # Your DeepSeek API token, fallback to CHAT_TOKEN env var
|
||||||
--debug(-d), # Debug mode
|
--debug(-d), # Debug mode
|
||||||
--repo(-r): string, # GitHub repo name, e.g. hustcer/deepseek-review, or local repo path / alias
|
--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
|
--pr-number(-n): string, # GitHub PR number
|
||||||
--gh-token(-k): string, # Your GitHub token, fallback to GITHUB_TOKEN env var
|
--gh-token(-k): string, # Your GitHub token, fallback to GITHUB_TOKEN env var
|
||||||
--diff-to(-t): string, # Diff to git REF
|
--diff-to(-t): string, # Git diff ending commit SHA
|
||||||
--diff-from(-f): string, # Diff from git REF
|
--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
|
--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.
|
--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
|
--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
|
--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,
|
--sys-prompt(-s): string # Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||||
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||||
--include(-i): string, # Comma separated file patterns to include in the code review
|
--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
|
--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 {
|
]: nothing -> nothing {
|
||||||
|
|
||||||
$env.config.table.mode = 'psql'
|
$env.config.table.mode = 'psql'
|
||||||
let local_repo = $env.PWD
|
let local_repo = $env.PWD
|
||||||
|
let write_file = ($output | is-not-empty)
|
||||||
let is_action = ($env.GITHUB_ACTIONS? == 'true')
|
let is_action = ($env.GITHUB_ACTIONS? == 'true')
|
||||||
let stream = if $is_action { false } else { true }
|
|
||||||
let token = $token | default $env.CHAT_TOKEN?
|
let token = $token | default $env.CHAT_TOKEN?
|
||||||
let repo = $repo | default $env.DEFAULT_GITHUB_REPO?
|
let repo = $repo | default $env.DEFAULT_GITHUB_REPO?
|
||||||
let CHAT_HEADER = [Authorization $'Bearer ($token)']
|
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 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 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 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 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 }
|
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
|
validate-temperature $temperature
|
||||||
let setting = {
|
let setting = {
|
||||||
repo: $repo,
|
repo: $repo,
|
||||||
@@ -109,7 +114,7 @@ export def --env deepseek-review [
|
|||||||
print $hint; print -n (char nl)
|
print $hint; print -n (char nl)
|
||||||
if ($pr_number | is-empty) {
|
if ($pr_number | is-empty) {
|
||||||
print 'Current Settings:'; hr-line
|
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 = (
|
let content = (
|
||||||
@@ -130,7 +135,8 @@ export def --env deepseek-review [
|
|||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: $sys_prompt },
|
{ role: 'system', content: $sys_prompt },
|
||||||
{ role: 'user', content: $"($user_prompt):\n($content)" }
|
{ role: 'user', content: $"($user_prompt):\n($content)" }
|
||||||
]
|
],
|
||||||
|
thinking: { type: 'disabled' }
|
||||||
}
|
}
|
||||||
if $debug { print $'(char nl)Code Changes:'; hr-line; print $content }
|
if $debug { print $'(char nl)Code Changes:'; hr-line; print $content }
|
||||||
print $'(char nl)Waiting for response from (ansi g)($url)(ansi reset) ...'
|
print $'(char nl)Waiting for response from (ansi g)($url)(ansi reset) ...'
|
||||||
@@ -146,27 +152,59 @@ export def --env deepseek-review [
|
|||||||
print $'✖️ Code review failed!Error: '; hr-line; print $response
|
print $'✖️ Code review failed!Error: '; hr-line; print $response
|
||||||
exit $ECODE.SERVER_ERROR
|
exit $ECODE.SERVER_ERROR
|
||||||
}
|
}
|
||||||
let message = $response | get -i choices.0.message
|
let message = $response | get -o choices.0.message
|
||||||
let reason = $message | coalesce-reasoning
|
let reason = $message | coalesce-reasoning
|
||||||
let review = $message.content? | default ($response | get -i message.content)
|
let review = $message.content? | default ($response | get -o message.content)
|
||||||
let result = ['<details>' '<summary> Reasoning Details</summary>' $reason "</details>\n" $review] | str join "\n"
|
let result = ['<details>' '<summary> Reasoning Details</summary>' $reason "</details>\n" $review] | str join "\n"
|
||||||
if ($review | is-empty) {
|
if ($review | is-empty) {
|
||||||
print $'✖️ Code review failed!No review result returned from ($base_url) ...'
|
print $'✖️ Code review failed!No review result returned from ($base_url) ...'
|
||||||
exit $ECODE.SERVER_ERROR
|
exit $ECODE.SERVER_ERROR
|
||||||
}
|
}
|
||||||
let result = if ($reason | is-empty) { $review } else { $result }
|
let result = if ($reason | is-empty) { $review } else { $result }
|
||||||
if not $is_action {
|
|
||||||
print $'Code Review Result:'; hr-line; print $result
|
match $output_mode {
|
||||||
} else {
|
'action' => {
|
||||||
post-comments-to-pr $repo $pr_number $result
|
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.'
|
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) {
|
if ($response.usage? | is-not-empty) {
|
||||||
print $'(char nl)Token Usage:'; hr-line
|
print $'(char nl)Token Usage:'; hr-line
|
||||||
$response.usage? | table -e | print
|
$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
|
# Validate the DeepSeek API token
|
||||||
def validate-token [token?: string, --pr-number: string, --repo: string] {
|
def validate-token [token?: string, --pr-number: string, --repo: string] {
|
||||||
if ($token | is-empty) {
|
if ($token | is-empty) {
|
||||||
@@ -227,10 +265,10 @@ def streaming-output [
|
|||||||
| try { lines } catch { print $'(ansi r)Error Happened ...(ansi reset)'; exit $ECODE.SERVER_ERROR }
|
| try { lines } catch { print $'(ansi r)Error Happened ...(ansi reset)'; exit $ECODE.SERVER_ERROR }
|
||||||
| each {|line|
|
| each {|line|
|
||||||
if ($line | is-empty) { return }
|
if ($line | is-empty) { return }
|
||||||
if ($IGNORED_MESSAGES | get -i $line | default false) { return }
|
if ($IGNORED_MESSAGES | get -o $line | default false) { return }
|
||||||
let $last = $line | parse-line
|
let $last = $line | parse-line
|
||||||
if $debug { $last | to json | kv set last-reply }
|
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
|
let delta = $in
|
||||||
if ($delta | coalesce-reasoning | 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 (kv get reasoning) == 1 { print $'(char nl)Reasoning Details:'; hr-line }
|
||||||
@@ -242,7 +280,7 @@ def streaming-output [
|
|||||||
|
|
||||||
if $debug and (kv get last-reply | is-not-empty) {
|
if $debug and (kv get last-reply | is-not-empty) {
|
||||||
print $'(char nl)(char nl)Model & Token Usage:'; hr-line
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export def is-safe-git [cmd: string] {
|
|||||||
let normalized_cmd = ($cmd | str trim | str downcase)
|
let normalized_cmd = ($cmd | str trim | str downcase)
|
||||||
|
|
||||||
# Define allowed command patterns with named capture groups for better validation
|
# 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}$'
|
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) {
|
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)'
|
print $'(ansi r)Invalid git command format. (ansi g)Only simple `git show` or `git diff` commands are allowed.(ansi reset)'
|
||||||
@@ -132,7 +132,7 @@ def install-gawk-for-actions [] {
|
|||||||
Invoke-Expression (New-Object System.Net.WebClient).DownloadString("https://get.scoop.sh")
|
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
|
$env:Path = "$env:USERPROFILE\scoop\shims;" + $env:Path; scoop update; scoop install gawk
|
||||||
'# | complete | get stdout | print
|
'# | complete | get stdout | print
|
||||||
let awk_bin = $'($nu.home-path)/scoop/shims/gawk.exe'
|
let awk_bin = $'($nu.home-dir)/scoop/shims/gawk.exe'
|
||||||
let version = get-awk-ver $awk_bin
|
let version = get-awk-ver $awk_bin
|
||||||
{ awk_bin: $awk_bin, version: $version }
|
{ awk_bin: $awk_bin, version: $version }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
|
|
||||||
use std/assert
|
use std/assert
|
||||||
|
use std/testing *
|
||||||
|
|
||||||
use ../nu/common.nu [
|
use ../nu/common.nu [
|
||||||
compare-ver, 'from env', is-installed, has-ref,
|
compare-ver, 'from env', is-installed, has-ref,
|
||||||
git-check, compact-record, is-repo, windows?, mac?,
|
git-check, compact-record, is-repo, windows?, mac?,
|
||||||
]
|
]
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'compare-ver:v1.0.0 is greater than v0.999.0' [] {
|
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 1.0.0 0.999.0) 1
|
||||||
assert equal (compare-ver v1.0.0 v0.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' [] {
|
def 'compare-ver:v1.0.1 is equal to v1.0.1' [] {
|
||||||
assert equal (compare-ver 1.0.1 1.0.1) 0
|
assert equal (compare-ver 1.0.1 1.0.1) 0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'compare-ver:v1.0.0 is equal to v1' [] {
|
def 'compare-ver:v1.0.0 is equal to v1' [] {
|
||||||
assert equal (compare-ver v1.0.0 v1) 0
|
assert equal (compare-ver v1.0.0 v1) 0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'compare-ver:v1.0.1 is greater than v1' [] {
|
def 'compare-ver:v1.0.1 is greater than v1' [] {
|
||||||
assert equal (compare-ver v1.0.1 v1) 1
|
assert equal (compare-ver v1.0.1 v1) 1
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'compare-ver:v1.0.1 is lower than v1.1.0' [] {
|
def 'compare-ver:v1.0.1 is lower than v1.1.0' [] {
|
||||||
assert less (compare-ver 1.0.1 v1.1) 0
|
assert less (compare-ver 1.0.1 v1.1) 0
|
||||||
assert equal (compare-ver 1.0.1 1.1.0) (-1)
|
assert equal (compare-ver 1.0.1 1.1.0) (-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'from-env:.env load should work' [] {
|
def 'from-env:.env load should work' [] {
|
||||||
open tests/resources/.env.test | from env | load-env
|
open tests/resources/.env.test | from env | load-env
|
||||||
assert equal $env.CHAT_MODEL deepseek-chat
|
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'
|
assert equal $env.USER_PROMPT 'Please review the following code changes'
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'is-installed:binary install check should work' [] {
|
def 'is-installed:binary install check should work' [] {
|
||||||
assert equal (is-installed git) true
|
assert equal (is-installed git) true
|
||||||
assert equal (is-installed abc) false
|
assert equal (is-installed abc) false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'has-ref:git repo should has HEAD ref' [] {
|
def 'has-ref:git repo should has HEAD ref' [] {
|
||||||
assert equal (has-ref HEAD) true
|
assert equal (has-ref HEAD) true
|
||||||
assert equal (has-ref 0000) false
|
assert equal (has-ref 0000) false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'is-repo:current dir is a git repo' [] {
|
def 'is-repo:current dir is a git repo' [] {
|
||||||
assert equal (is-repo) true
|
assert equal (is-repo) true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'git-check:current dir is a git repo' [] {
|
def 'git-check:current dir is a git repo' [] {
|
||||||
assert equal (git-check (pwd) --check-repo=1) true
|
assert equal (git-check (pwd) --check-repo=1) true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'compact-record:should work as expected' [] {
|
def 'compact-record:should work as expected' [] {
|
||||||
assert equal ({a: null, b: '', c: 'abc' } | compact-record) { c: 'abc' }
|
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' } }
|
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' [] {
|
def 'OS check should work as expected' [] {
|
||||||
# `$env.RUNNER_OS` Possible values are Linux, Windows, or macOS in GitHub Actions
|
# `$env.RUNNER_OS` Possible values are Linux, Windows, or macOS in GitHub Actions
|
||||||
match $nu.os-info.name {
|
match $nu.os-info.name {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
|
||||||
use std/assert
|
use std/assert
|
||||||
|
use std/testing *
|
||||||
use ../nu/diff.nu [get-diff]
|
use ../nu/diff.nu [get-diff]
|
||||||
use ../nu/util.nu [is-safe-git, prepare-awk, generate-include-regex, generate-exclude-regex]
|
use ../nu/util.nu [is-safe-git, prepare-awk, generate-include-regex, generate-exclude-regex]
|
||||||
|
|
||||||
# Get the unicode width of the input string
|
# Get the unicode width of the input string
|
||||||
def get-uw [] { $in | str stats | get unicode-width }
|
def get-uw [] { $in | str stats | get unicode-width }
|
||||||
|
|
||||||
#[before-all]
|
@before-all
|
||||||
def setup [] {
|
def setup [] {
|
||||||
let awk_bin = (prepare-awk)
|
let awk_bin = (prepare-awk)
|
||||||
let patch = open -r tests/resources/diff.patch
|
let patch = open -r tests/resources/diff.patch
|
||||||
@@ -14,7 +15,7 @@ def setup [] {
|
|||||||
{ patch: $patch, awk: $awk_bin, SHA: 22e7b71 }
|
{ patch: $patch, awk: $awk_bin, SHA: 22e7b71 }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'is-safe-git:should work as expected' [] {
|
def 'is-safe-git:should work as expected' [] {
|
||||||
assert equal (is-safe-git 'git diff') true
|
assert equal (is-safe-git 'git diff') true
|
||||||
assert equal (is-safe-git 'git show') true
|
assert equal (is-safe-git 'git show') true
|
||||||
@@ -41,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/* >> 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 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' [] {
|
def 'generate-include-regex:should work as expected' [] {
|
||||||
let patch = $in.patch
|
let patch = $in.patch
|
||||||
let awk_bin = $in.awk
|
let awk_bin = $in.awk
|
||||||
@@ -53,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
|
assert equal ($patch | ^$awk_bin (generate-include-regex [.env*, *.md, nu/*]) | get-uw) 6871
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'generate-exclude-regex:should work as expected' [] {
|
def 'generate-exclude-regex:should work as expected' [] {
|
||||||
let patch = $in.patch
|
let patch = $in.patch
|
||||||
let awk_bin = $in.awk
|
let awk_bin = $in.awk
|
||||||
@@ -61,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)
|
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' [] {
|
def 'both include and exclude should work as expected' [] {
|
||||||
let patch = $in.patch
|
let patch = $in.patch
|
||||||
let awk_bin = $in.awk
|
let awk_bin = $in.awk
|
||||||
@@ -71,7 +74,7 @@ def 'both include and exclude should work as expected' [] {
|
|||||||
| get-uw) 2576
|
| get-uw) 2576
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'both exclude and include should work as expected' [] {
|
def 'both exclude and include should work as expected' [] {
|
||||||
let patch = $in.patch
|
let patch = $in.patch
|
||||||
let awk_bin = $in.awk
|
let awk_bin = $in.awk
|
||||||
@@ -81,7 +84,7 @@ def 'both exclude and include should work as expected' [] {
|
|||||||
| get-uw) 2576
|
| get-uw) 2576
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'get-diff:get patch from remote PR should work' [] {
|
def 'get-diff:get patch from remote PR should work' [] {
|
||||||
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
||||||
const repo = 'hustcer/deepseek-review'
|
const repo = 'hustcer/deepseek-review'
|
||||||
@@ -91,7 +94,7 @@ def 'get-diff:get patch from remote PR should work' [] {
|
|||||||
| str join "\n" | get-uw) 7923
|
| str join "\n" | get-uw) 7923
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'get-diff:get patch from remote PR with include should work' [] {
|
def 'get-diff:get patch from remote PR with include should work' [] {
|
||||||
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
||||||
const repo = 'hustcer/deepseek-review'
|
const repo = 'hustcer/deepseek-review'
|
||||||
@@ -100,7 +103,7 @@ def 'get-diff:get patch from remote PR with include should work' [] {
|
|||||||
assert equal ($patch | get-uw) 2576
|
assert equal ($patch | get-uw) 2576
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'get-diff:get patch from remote PR with exclude should work' [] {
|
def 'get-diff:get patch from remote PR with exclude should work' [] {
|
||||||
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
||||||
const repo = 'hustcer/deepseek-review'
|
const repo = 'hustcer/deepseek-review'
|
||||||
@@ -109,7 +112,7 @@ def 'get-diff:get patch from remote PR with exclude should work' [] {
|
|||||||
assert equal ($patch | get-uw) 555
|
assert equal ($patch | get-uw) 555
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
@test
|
||||||
def 'get-diff:get patch from remote PR with exclude & include should work' [] {
|
def 'get-diff:get patch from remote PR with exclude & include should work' [] {
|
||||||
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
$env.GH_TOKEN = $env.GITHUB_TOKEN?
|
||||||
const repo = 'hustcer/deepseek-review'
|
const repo = 'hustcer/deepseek-review'
|
||||||
|
|||||||
Reference in New Issue
Block a user