1
0
mirror of https://github.com/hustcer/deepseek-review.git synced 2026-05-13 05:16:05 +08:00

32 Commits
v1.6 ... v1.11

Author SHA1 Message Date
hustcer
f3ea289921 Bump to v1.11 2025-02-13 19:48:21 +08:00
Justin Ma
4af2f6036b fix: Fix include & exclude pattern matching error (#122)
* fix: Fix include & exclude pattern matching error

* ci skip

* code refactor
2025-02-13 19:44:43 +08:00
hustcer
3dd6b23201 fix: Fix reasoning_content field may not exists error 2025-02-13 15:45:09 +08:00
Justin Ma
c2b26fdc30 fix: Do not show reasoning parts if reviewed by V3 model (#121) 2025-02-13 15:29:10 +08:00
hustcer
06d80785b1 Bump to v1.10 2025-02-12 23:30:23 +08:00
Justin Ma
84dbd263c9 fix: Improve error handling for API response in streaming-output (#117)
* fix: Improve error handling for API response in streaming-output

* Fix token error

* Fix error
2025-02-12 23:24:43 +08:00
hustcer
c0d78b92b9 feat: Separate the reasoning and the content part in the output of DeepSeek (#115) 2025-02-12 19:30:26 +08:00
hustcer
5652f31856 perf: Save the last reply of the model in memory instead of a file (#113) 2025-02-12 18:26:40 +08:00
hustcer
68e582fb95 Update README 2025-02-12 17:40:54 +08:00
Justin Ma
534d6230b7 chore: Refine DeepSeek review output, clarify debug logs, and adjust error messaging for better context (#111) 2025-02-12 14:26:39 +08:00
Justin Ma
6af37c1d98 chore: Make awk version check for both awk and gawk (#109) 2025-02-12 12:21:03 +08:00
hustcer
9b1a390493 Bump to v1.9 2025-02-12 12:04:57 +08:00
Justin Ma
4249b38d5b Test DeepSeek R1 model (#108)
* Test DeepSeek R1 model

* ci skip

* ci skip
2025-02-12 12:01:48 +08:00
hustcer
12e1537975 Try using Infinigence's DeepSeek Model 2025-02-12 11:52:53 +08:00
hustcer
fd3a97c3b6 Test DeepSeek-R1 model 2025-02-12 09:33:23 +08:00
Justin Ma
d84f126292 feat: Add DeepSeek R1 model support (#107)
* Try to add R1 support

* fix: Fix reasoning model output
2025-02-12 09:29:53 +08:00
Justin Ma
8dde3bd95f feat: Add support for custom patch commands in local code review (#106)
* feat: Add `is_safe_git` to validate git commands for safety in shell

* feat: Add support for custom patch commands in local code review

* Update docs
2025-02-11 17:06:49 +08:00
Justin Ma
2288595e98 feat: Add streaming output support for local code review (#103) 2025-02-11 14:11:23 +08:00
Justin Ma
d064f64443 fix: Fix no repo column error for local code review (#102) 2025-02-11 13:10:04 +08:00
hustcer
c3d18ccb03 doc: Update README 2025-02-10 18:19:27 +08:00
hustcer
2393375400 Bump to v1.8 2025-02-10 16:54:30 +08:00
Justin Ma
22e7b71776 feat: Add support for configurable temperature parameter in DeepSeek model setup (#93)
* feat: Add support for configurable temperature parameter in DeepSeek model setup

* chore: Add temperature validation
2025-02-10 16:41:22 +08:00
hustcer
798baee3d8 chore: Update README add awk or gawk as required tools 2025-02-10 16:03:31 +08:00
Justin Ma
b19e2097cc fix: Add awk binary missing check (#92) 2025-02-10 15:28:08 +08:00
Justin Ma
699ebd30f0 feat: Add version validation for awk/gawk and implement robust semantic version comparison for compatibility checks (#91)
feat: Add version validation for awk/gawk and implement robust semantic version comparison for compatibility checks (#91), fixes #85
2025-02-10 11:54:47 +08:00
Justin Ma
a12b9c6fdd fix: Add check for empty DeepSeek review response with error handling (#90) 2025-02-10 10:29:05 +08:00
Justin Ma
44b44de9b1 refactor: Streamline main wrapper and simplify argument handling for nu/review.nu integration (#88) 2025-02-10 10:21:04 +08:00
hustcer
c17a3836e8 ci skip 2025-02-08 21:55:52 +08:00
hustcer
5c6cbc3c92 chore: Update README 2025-02-08 18:39:10 +08:00
hustcer
88206a92f0 Bump to v1.7 2025-02-08 18:16:11 +08:00
Justin Ma
2ab221a624 feat: Remove the dependency on just for local code review (#84)
* feat: Remove the dependency on just for local code review

* chore: Try to fix post comments
2025-02-08 11:54:56 +08:00
Justin Ma
cc5bb6bb4c refactor: Improve prompts loading helper (#82) 2025-02-08 10:06:25 +08:00
12 changed files with 642 additions and 86 deletions

View File

@@ -11,6 +11,8 @@ CHAT_TOKEN='Your DeepSeek API token'
GITHUB_TOKEN='Your GitHub API token'
# MAX_LENGTH: The maximum length of the content for review, 0 means no limit.
MAX_LENGTH='0'
# The maximum temperature for the model to generate the response. 1.0 by default.
TEMPERATURE='1.0'
# The comma separated file patterns to include in the code review.
INCLUDE_PATTERNS=''
# The comma separated file patterns to exclude in the code review.

View File

@@ -25,9 +25,12 @@ jobs:
uses: hustcer/deepseek-review@develop
with:
max-length: 15000
# SiliconFlow's DeepSeek model
model: 'deepseek-ai/DeepSeek-V3'
base-url: 'https://api.siliconflow.cn/v1'
# model: 'deepseek-v3' # Infinigence's DeepSeek V3 model
model: 'deepseek-r1' # Infinigence's DeepSeek R1 model
base-url: 'https://cloud.infini-ai.com/maas/v1' # Infinigence's API base URL
# model: 'deepseek-ai/DeepSeek-V3' # SiliconFlow's DeepSeek V3 model
# model: 'deepseek-ai/DeepSeek-R1' # SiliconFlow's DeepSeek R1 model
# base-url: 'https://api.siliconflow.cn/v1' # SiliconFlow's API base URL
# Store the chat token in GitHub Secrets, don't expose it in the workflow file
chat-token: ${{ secrets.CHAT_TOKEN }}
sys-prompt: >

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
.env
.env.dev
.env.local
config.yml
prompts.yaml

View File

@@ -1,6 +1,79 @@
# Changelog
All notable changes to this project will be documented in this file.
## [1.11.0] - 2025-02-13
### Bug Fixes
- Do not show reasoning parts if reviewed by DeepSeek V3 model (#121)
- Fix `reasoning_content` field may not exists error
- Fix `include` & `exclude` pattern matching error (#122)
## [1.10.0] - 2025-02-12
### Features
- Separate the reasoning and the content part of the output (#115)
### Bug Fixes
- Improve error handling of API response for streaming-output (#117)
### Miscellaneous Tasks
- Make `awk` version check works for both `awk` and `gawk` (#109)
- Refine DeepSeek review output, clarify debug logs, and adjust error messaging for better context (#111)
### Performance
- Save the last reply of the model to db instead of a file (#113)
## [1.9.0] - 2025-02-12
### Bug Fixes
- Fix no repo column error for local code review (#102)
### Features
- Add streaming output support for local code review (#103)
- Add support for custom patch commands by `-c, --patch-cmd` flag in local code review (#106)
- Add DeepSeek R1 model support (#107)
## [1.8.0] - 2025-02-10
### Bug Fixes
- Add check for empty DeepSeek review response with error handling (#90)
- Add `awk` binary missing check (#92)
### Features
- Add version validation for `awk`/`gawk` and implement robust semantic version comparison for compatibility checks (#91)
- Add support for configurable `temperature` parameter in DeepSeek model setup (#93)
### Miscellaneous Tasks
- Update README add `awk` or `gawk` as required tools
### Refactor
- Streamline main wrapper and simplify argument handling for `nu/review.nu` integration (#88)
## [1.7.0] - 2025-02-08
### Features
- Remove the dependency on `just` for local code review (#84)
### Bug Fixes
- Fix possible GitHub comment posting errors
### Refactor
- Improve prompts loading helper (#82)
## [1.6.0] - 2025-02-07
### Features

View File

@@ -2,12 +2,16 @@
[中文说明](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
- Automate PR Reviews with DeepSeek via GitHub Action
- Review Remote GitHub PRs Directly from Your Local CLI
- Analyze Commit Changes with DeepSeek for Any Local Repository with CLI
- Streaming Output Support for Local Code Review
- Fully Customizable: Choose Models, Base URLs, and Prompts
- Support Both DeepSeek's V3 & R1 Models
- Supports Self-Hosted DeepSeek Models for Enhanced Flexibility
- Perform Code Reviews for Changes That either Include or Exclude Specific Files
- Add `skip cr` or `skip review` to PR title or body to disable code review in GitHub Actions
@@ -16,6 +20,7 @@
## Planned Features
- [ ] **Trigger Code Review on Mention**: Automatically initiate code review when the `github-actions` bot is mentioned in a PR comment.
- [ ] **Generate Commit Message Locally**: Generate a commit message for the code changes in any local repository.
## Code Review with GitHub Action
@@ -100,11 +105,12 @@ 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-chat` |
| 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. |
| 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 |
| 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` |
| github-token | String | Optional, The `GITHUB_TOKEN` secret or personal access token to authenticate. Defaults to `github.token`. |
@@ -116,6 +122,7 @@ With this setup, DeepSeek code review will not run automatically upon PR creatio
// `$model` default value: deepseek-chat
model: $model,
stream: false,
temperature: $temperature,
messages: [
// `$sys_prompt` default value: You are a professional code review assistant responsible for
// analyzing code changes in GitHub Pull Requests. Identify potential issues such as code
@@ -141,14 +148,15 @@ With this setup, DeepSeek code review will not run automatically upon PR creatio
To perform code reviews locally(should works for `macOS`, `Ubuntu`, and `Windows`), you need to install the following tools:
- [`Nushell`](https://www.nushell.sh/book/installation.html) & [`Just`](https://just.systems/man/en/packages.html). It is recommended to install the latest versions.
- Once the tools are installed, simply clone this repository to your local machine, navigate to the repository directory, and run `just code-review -h` or `just cr -h`. You should see an output similar to the following:
- [`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.
- 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
Use DeepSeek AI to review code changes locally or in GitHub Actions
Usage:
> deepseek-review {flags} (token)
> nu cr {flags} (token)
Flags:
-d, --debug: Debug mode
@@ -157,13 +165,15 @@ Flags:
-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
-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, deepseek-chat by default (default: 'deepseek-chat')
-b, --base-url <string> (default: 'https://api.deepseek.com')
-m, --model <string>: Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
-b, --base-url <string>: DeepSeek API base URL, fallback to BASE_URL env var
-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`
-h, --help: Display the help message for this command
Parameters:
@@ -185,15 +195,22 @@ To perform code reviews locally, you need to modify the configuration file. A sa
```sh
# Perform code review on the `git diff` changes in the local DEFAULT_LOCAL_REPO repo
just cr
nu cr
# Perform code review on the `git diff f536acc` changes in the local DEFAULT_LOCAL_REPO repo
just cr --diff-from f536acc
nu cr --diff-from f536acc
# Perform code review on the `git diff f536acc 0dd0eb5` changes in the local DEFAULT_LOCAL_REPO repo
just cr --diff-from f536acc --diff-to 0dd0eb5
nu cr --diff-from f536acc --diff-to 0dd0eb5
# Review the changes in the local `DEFAULT_LOCAL_REPO` repo using the `--patch-cmd` flag
nu cr --patch-cmd 'git diff head~3'
nu cr -c 'git show head~3'
nu cr -c 'git diff 2393375 71f5a31'
nu cr -c 'git diff 2393375 71f5a31 nu/*'
nu cr -c 'git diff 2393375 71f5a31 :!nu/*'
# Dangerous commands like `nu cr -c 'git show head~3; rm ./*'` will not be allowed
# Perform code review on PR #31 in the remote DEFAULT_GITHUB_REPO repo
just cr --pr-number 31
nu cr --pr-number 31
# Perform code review on PR #31 in the remote hustcer/deepseek-review repo
just cr --pr-number 31 --repo hustcer/deepseek-review
nu cr --pr-number 31 --repo hustcer/deepseek-review
```
## License

View File

@@ -1,11 +1,15 @@
# DeepSeek 代码审查
本工具也支持使用 SiliconCloud 上的 DeepSeek 模型,[注册](https://cloud.siliconflow.cn/i/rqCdIxzS) 就**免费赠送 2000 万 Token**,赶紧试试吧!
## 特性
- 通过 GitHub Action 使用 DeepSeek 进行自动化 PR 审查
- 通过本地 CLI 直接审查远程 GitHub PR
- 通过本地 CLI 使用 DeepSeek 分析任何本地仓库的提交变更
- 本地代码审查的时候支持流式输出
- 完全可定制:选择模型、基础 URL 和提示词
- 支持 DeepSeek V3 和 R1 模型
- 支持自托管 DeepSeek 模型,提供更强的灵活性
- 在 PR 的标题或描述中添加 `skip cr` or `skip review` 可跳过 GitHub Actions 里的代码审查
- 对指定文件变更进行包含/排除式代码审查
@@ -14,6 +18,7 @@
## 计划支持特性
- [ ] **通过提及触发代码审查**:当 PR 评论中提及 `github-actions bot` 时,自动触发代码审查
- [ ] **本地生成提交信息**:为本地仓库的代码变更生成 Commit Message
## 通过 GitHub Action 进行代码审查
@@ -103,6 +108,7 @@ jobs:
| max-length | Int | 可选,待审查内容的最大 Unicode 长度, 默认 `0` 表示没有限制,超过非零值则跳过审查 |
| sys-prompt | String | 可选,系统提示词对应入参中的 `$sys_prompt`, 默认值见后文注释 |
| user-prompt | String | 可选,用户提示词对应入参中的 `$user_prompt`, 默认值见后文注释 |
| temperature | Number | 可选,采样温度,介于 `0``2` 之间, 默认值 `1.0` |
| include-patterns | String | 可选,代码审查中要包含的以逗号分隔的文件模式,无默认值 |
| exclude-patterns | String | 可选,代码审查中要排除的以逗号分隔的文件模式,默认值为 `pnpm-lock.yaml,package-lock.json,*.lock` |
| github-token | String | 可选,用于访问 API 进行 PR 管理的 GitHub Token默认为 `${{ github.token }}` |
@@ -114,6 +120,7 @@ DeepSeek 接口调用入参:
// `$model` default value: deepseek-chat
model: $model,
stream: false,
temperature: $temperature,
messages: [
// `$sys_prompt` default value: You are a professional code review assistant responsible for
// analyzing code changes in GitHub Pull Requests. Identify potential issues such as code
@@ -138,14 +145,15 @@ DeepSeek 接口调用入参:
在本地进行代码审查,支持 `macOS`, `Ubuntu` & `Windows` 不过需要安装以下工具:
- [`Nushell`](https://www.nushell.sh/book/installation.html) & [`Just`](https://just.systems/man/en/packages.html), 建议安装最新版本
- 接下来只需要把本仓库代码克隆到本地,然后进入仓库目录执行 `just code-review -h` 或者 `just cr -h` 即可看到类似如下输出:
- [`Nushell`](https://www.nushell.sh/book/installation.html), 建议安装最新版本
- [`awk`](https://github.com/onetrueawk/awk) 或者 [`gawk`](https://www.gnu.org/software/gawk/) 的最新版版本,优先推荐 `gawk`
- 接下来只需要把本仓库代码克隆到本地,然后进入仓库目录执行 `nu cr -h` 即可看到类似如下输出:
```console
Use DeepSeek AI to review code changes locally or in GitHub Actions
Usage:
> deepseek-review {flags} (token)
> nu cr {flags} (token)
Flags:
-d, --debug: Debug mode
@@ -154,13 +162,15 @@ Flags:
-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
-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, deepseek-chat by default (default: 'deepseek-chat')
-b, --base-url <string> (default: 'https://api.deepseek.com')
-m, --model <string>: Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
-b, --base-url <string>: DeepSeek API base URL, fallback to BASE_URL env var
-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`
-h, --help: Display the help message for this command
Parameters:
@@ -181,15 +191,22 @@ Parameters:
```sh
# 对本地 DEFAULT_LOCAL_REPO 仓库 `git diff` 修改内容进行代码审查
just cr
nu cr
# 对本地 DEFAULT_LOCAL_REPO 仓库 `git diff f536acc` 修改内容进行代码审查
just cr --diff-from f536acc
nu cr --diff-from f536acc
# 对本地 DEFAULT_LOCAL_REPO 仓库 `git diff f536acc 0dd0eb5` 修改内容进行代码审查
just cr --diff-from f536acc --diff-to 0dd0eb5
nu cr --diff-from f536acc --diff-to 0dd0eb5
# 通过 --patch-cmd 参数对本地 DEFAULT_LOCAL_REPO 仓库变更内容进行审查
nu cr --patch-cmd 'git diff head~3'
nu cr -c 'git show head~3'
nu cr -c 'git diff 2393375 71f5a31'
nu cr -c 'git diff 2393375 71f5a31 nu/*'
nu cr -c 'git diff 2393375 71f5a31 :!nu/*'
# 像 `nu cr -c 'git show head~3; rm ./*'` 这样危险的命令将会被禁止
# 对远程 DEFAULT_GITHUB_REPO 仓库编号为 31 的 PR 进行代码审查
just cr --pr-number 31
nu cr --pr-number 31
# 对远程 hustcer/deepseek-review 仓库编号为 31 的 PR 进行代码审查
just cr --pr-number 31 --repo hustcer/deepseek-review
nu cr --pr-number 31 --repo hustcer/deepseek-review
```
## 许可

View File

@@ -26,6 +26,10 @@ inputs:
required: false
default: 'deepseek-chat'
description: 'The DeepSeek model to choose for code review.'
temperature:
required: false
default: 1.0
description: 'The temperature of the model.'
base-url:
required: false
default: 'https://api.deepseek.com'
@@ -74,6 +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 }
(deepseek-review $token
--model $model
--repo $repo
@@ -83,6 +88,7 @@ runs:
--max-length $maxLength
--sys-prompt $sysPrompt
--user-prompt $userPrompt
--temperature $temperature
--include $includePatterns
--exclude $excludePatterns
)

16
cr Normal file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env nu
# Author: hustcer
# Created: 2025/02/08 19:02:15
# Description: A wrapper for nu/review.nu as the main entry point of the project.
use nu/review.nu ['from env', ECODE]
# Use DeepSeek AI to review code changes locally or in GitHub Actions
def --wrapped main [...rest] {
if not ('.env' | path exists) {
print $'Please refer to (ansi g)`.env.example`(ansi reset) to create a (ansi r)`.env`(ansi reset) file in the root directory of the project.'
exit $ECODE.MISSING_DEPENDENCY
}
open .env | load-env
nu $'($env.FILE_PWD)/nu/review.nu' ...$rest
}

View File

@@ -5,6 +5,9 @@
words:
- psql
- MPFR
- nuon
- nuons
- ECODE
- endfor
- dotenv
@@ -16,4 +19,5 @@ words:
- linewise
- Subshell
- subshells
- Infinigence
ignorePaths:

View File

@@ -1,7 +1,7 @@
{
"name": "deepseek-review",
"version": "1.6.0",
"actionVer": "v1.6",
"version": "1.11.0",
"actionVer": "v1.11",
"author": "hustcer",
"license": "MIT",
"github": "https://github.com/hustcer/deepseek-review",

187
nu/kv.nu Normal file
View File

@@ -0,0 +1,187 @@
# 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}}
}
}

View File

@@ -7,6 +7,8 @@
# [√] Debug mode
# [√] Output token usage info
# [√] Perform CR for changes that either include or exclude specific files
# [√] Support streaming output for local code review
# [√] Support using custom patch command to get diff content
# [ ] Add more action outputs
# Description: A script to do code review by DeepSeek
# REF:
@@ -23,8 +25,10 @@
# - Local Repo Review: just cr -f HEAD~1 --debug
# - Local PR Review: just cr -r hustcer/deepseek-review -n 32
use kv.nu *
# Commonly used exit codes
const ECODE = {
export const ECODE = {
SUCCESS: 0,
OUTDATED: 1,
AUTH_FAILED: 2,
@@ -35,6 +39,8 @@ const ECODE = {
CONDITION_NOT_SATISFIED: 8,
}
const RESPONSE_END = 'data: [DONE]'
const GITHUB_API_BASE = 'https://api.github.com'
# It takes longer to respond to requests made with unknown/rare user agents.
@@ -43,6 +49,7 @@ const HTTP_HEADERS = [User-Agent curl/8.9]
const DEFAULT_OPTIONS = {
MODEL: 'deepseek-chat',
TEMPERATURE: 1.0,
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.',
@@ -60,6 +67,7 @@ export def --env deepseek-review [
--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
--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
--base-url(-b): string, # DeepSeek API base URL, fallback to BASE_URL env var
@@ -67,16 +75,24 @@ export def --env deepseek-review [
--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`, Only for V3
]: nothing -> nothing {
$env.config.table.mode = 'psql'
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 local_repo = $env.DEFAULT_LOCAL_REPO? | default (pwd)
let model = $model | default $env.CHAT_MODEL? | default $DEFAULT_OPTIONS.MODEL
let max_length = $max_length | default ($env.MAX_LENGTH? | default 0 | into int)
let base_url = $base_url | default $env.BASE_URL? | default $DEFAULT_OPTIONS.BASE_URL
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 }
if ($temperature < 0) or ($temperature > 2) {
print $'(ansi r)Invalid temperature value, should be in the range of 0 to 2.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
let url = $'($base_url)/chat/completions'
let setting = {
repo: $repo,
@@ -86,11 +102,14 @@ export def --env deepseek-review [
exclude: $exclude,
diff_to: $diff_to,
diff_from: $diff_from,
patch_cmd: $patch_cmd,
pr_number: $pr_number,
max_length: $max_length,
local_repo: $local_repo,
temperature: $temperature,
}
$env.GH_TOKEN = $gh_token | default $env.GITHUB_TOKEN?
if ($token | is-empty) {
print $'(ansi r)Please provide your DeepSeek API token by setting `CHAT_TOKEN` or passing it as an argument.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
@@ -101,11 +120,14 @@ export def --env deepseek-review [
$'🚀 Initiate the code review by DeepSeek AI for PR (ansi g)#($pr_number)(ansi reset) in (ansi g)($repo)(ansi reset) ...'
}
print $hint; print -n (char nl)
if ($pr_number | is-empty) { $setting | compact-record | reject repo | print }
if ($pr_number | is-empty) {
print 'Current Settings:'; hr-line
$setting | compact-record | reject -i repo | print; print -n (char nl)
}
let content = (
get-diff --pr-number $pr_number --repo $repo --diff-to $diff_to
--diff-from $diff_from --include $include --exclude $exclude)
--diff-from $diff_from --include $include --exclude $exclude --patch-cmd $patch_cmd)
let length = $content | str stats | get unicode-width
if ($max_length != 0) and ($length > $max_length) {
print $'(char nl)(ansi r)The content length ($length) exceeds the maximum limit ($max_length), review skipped.(ansi reset)'
@@ -116,49 +138,107 @@ export def --env deepseek-review [
let user_prompt = $user_prompt | default (load-prompt-from-env USER_PROMPT) | default $DEFAULT_OPTIONS.USER_PROMPT
let payload = {
model: $model,
stream: false,
stream: $stream,
temperature: $temperature,
messages: [
{ role: 'system', content: $sys_prompt },
{ role: 'user', content: $"($user_prompt):\n($content)" }
]
}
if $debug { print $'Code Changes:'; hr-line; print $content }
print $'(char nl)Waiting for response from (ansi g)($url)(ansi reset) ...'
if $debug { print $'(char nl)Code Changes:'; hr-line; print $content }
print $'(char nl)Waiting for response from (ansi g)($base_url)(ansi reset) ...'
if $stream { streaming-output $url $payload --headers $CHAT_HEADER --debug=$debug; return }
let response = http post -e -H $CHAT_HEADER -t application/json $url $payload
if ($response | is-empty) {
print $'(ansi r)Oops, No response returned from DeepSeek API.(ansi reset)'
print $'(ansi r)Oops, No response returned from ($base_url) ...(ansi reset)'
exit $ECODE.SERVER_ERROR
}
if $debug { print $'DeepSeek Response:'; hr-line; $response | table -e | print }
if $debug { print $'DeepSeek Model Response:'; hr-line; $response | table -e | print }
if ($response | describe) == 'string' {
print $' Code review failedError: '; hr-line; print $response
print $'✖️ Code review failedError: '; 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
let result = ['<details>' '<summary> Reasoning Details</summary>' $reason "</details>\n" $review] | str join "\n"
if ($review | is-empty) {
print $'✖️ Code review failedNo 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 $review
print $'Code Review Result:'; hr-line; print $result
} else {
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json ...$HTTP_HEADERS]
http post -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/issues/($pr_number)/comments' ({ body: $review } | to json)
http post -t application/json -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/issues/($pr_number)/comments' { body: $result }
print $'✅ Code review finishedPR (ansi g)#($pr_number)(ansi reset) review result was posted as a comment.'
}
print $'(char nl)Token Usage Info:'; hr-line
print $'(char nl)Token Usage:'; hr-line
$response.usage | table -e | print
}
# Output the streaming response of review result from DeepSeek API
def streaming-output [
url: string, # The Full DeepSeek API URL
payload: record, # The payload to send to DeepSeek API
--debug, # Debug mode
--headers: list, # The headers to send to DeepSeek API
] {
print -n (char nl)
kv set content 0
kv set reasoning 0
http post -e -H $headers -t application/json $url $payload
| tee {
let res = $in
let type = $res | describe
let record_error = $type =~ '^record'
let other_error = $type =~ '^string' and $res !~ 'data: '
if $record_error or $other_error {
$res | table -e | print
exit $ECODE.SERVER_ERROR
}
}
| 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 }
let $last = $line | str substring 6.. | from json
if $last == '-alive' { print $last; return }
if $debug { $last | to json | kv set last-reply }
$last | get -i choices.0.delta | if ($in | is-not-empty) {
let delta = $in
if ($delta.reasoning_content? | 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)
}
}
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
}
}
# Load the prompt content from the specified env var
export def load-prompt-from-env [
prompt_key: string,
] {
let prompt = $env | get -i $prompt_key | default ''
if $prompt =~ '.yaml' {
let key = $prompt | split row : | last
let path = $prompt | split row : | first
try { open $path | get -i $key } catch {
print $'(ansi r)Failed to load the prompt content from ($path), please check it again.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
} else { $prompt }
if ($prompt !~ '.ya?ml') { return $prompt }
let parts = $prompt | split row :
if ($parts | length) != 2 {
print $'(ansi r)Invalid prompt format: expected path:key for YAML files.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
let key = $parts | last
let path = $parts | first
try { open $path | get -i $key } catch {
print $'(ansi r)Failed to load the prompt content from ($path), please check it again.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
}
# Get the diff content from GitHub PR or local git changes
@@ -169,6 +249,7 @@ export def get-diff [
--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 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]
@@ -179,59 +260,83 @@ export def get-diff [
}
cd $local_repo
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
}
# 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 { git diff }
if ($repo | is-empty) {
print $'(ansi r)Please provide the GitHub repository name by `--repo` option.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
# 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
}
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 | ^$awk_bin (generate-include-regex $patterns)
$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 | ^$awk_bin (generate-exclude-regex $patterns)
$content = $content | try { ^$awk_bin (generate-exclude-regex $patterns) } catch { print $outdated_awk; exit $ECODE.OUTDATED }
}
$content
}
# Prepare gawk for macOS
export def prepare-awk [] {
if (is-installed awk) {
print $'Current awk version: (awk --version | lines | first)'
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 {
# 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
let awk_version = awk --version | lines | first | split row , | first | split row ' ' | last
print $'Current awk version: ($awk_version)'
if (compare-ver $awk_version $MIN_AWK_VERSION) >= 0 { return 'awk' }
}
if ($env.GITHUB_ACTIONS? != 'true') { return 'awk' }
if (sys host | get name) == 'Darwin' {
if $gawk_installed {
let gawk_version = gawk --version | lines | first | split row , | first | split row ' ' | last
print $'Current gawk version: ($gawk_version)'
if (compare-ver $gawk_version $MIN_GAWK_VERSION) >= 0 { return 'gawk' }
}
if (sys host | get name) == 'Darwin' and (is-installed brew) {
brew install gawk
print $'Current gawk version: (gawk --version | lines | first)'
return 'gawk'
}
'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
}
'awk'
}
# Compact the record by removing empty columns
@@ -301,19 +406,142 @@ export def hr-line [
}
print $'(ansi $color)(build-line $width)(if $with_arrow {'>'})(ansi reset)'
if $blank_line { char nl }
if $blank_line { char nl | print -n }
}
# 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>] {
$patterns
| each { |pat|
$pat | str replace "*" ".*" | str replace "?" "." | str replace "/" "\\/"
}
| str join "|"
}
# Generate the awk include regex pattern string for the specified patterns
export def generate-include-regex [patterns: list<string>] {
let pattern = $patterns | each {|pat| $pat | str replace '/' '\/' } | str join '|'
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
def generate-exclude-regex [patterns: list<string>] {
let pattern = $patterns | each {|pat| $pat | str replace '/' '\/' } | str join '|'
export def generate-exclude-regex [patterns: list<string>] {
let pattern = glob-to-regex $patterns
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}!p"
}
# 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
}
# Compare two version number, return `1` if first one is higher than second one,
# Return `0` if they are equal, otherwise return `-1`
# Format: Expects semantic version strings (major.minor.patch)
# - Optional 'v' prefix
# - Pre-release suffixes (-beta, -rc, etc.) are ignored
# - Missing segments default to 0
export def compare-ver [v1: string, v2: string] {
# Parse the version number: remove pre-release and build information,
# only take the main version part, and convert it to a list of numbers
def parse-ver [v: string] {
$v | str downcase | str trim -c v | str trim
| split row - | first | split row . | each { into int }
}
let a = parse-ver $v1
let b = parse-ver $v2
# Compare the major, minor, and patch parts; fill in the missing parts with 0
# 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
if $x > $y { return 1 }
if $x < $y { return (-1) }
}
0
}
# 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 --since=2025-02-09 HEAD
# - git diff 2393375 71f5a31
# - git diff 2393375 71f5a31 nu/*
# - git diff 2393375 71f5a31 :!nu/*
export def is-safe-git [cmd: string] {
# Normalize the command string by trimming and converting to lowercase
let normalized_cmd = ($cmd | str trim | str downcase)
# More strict regex for git commands, allow:
# 1. --since parameter with ISO date format
# 2. File path patterns with or without colon (e.g. :!nu/*, nu/*)
let allowed_regex = '^git\s+(show|diff)(?:\s+(?:--since=\d{4}-\d{2}-\d{2}|[a-zA-Z0-9_\-\.~/]+))*(?:\s+(?::[!]?)?[a-zA-Z0-9_\-\.\*\/]+)?$'
# Dangerous patterns to check (expanded list)
let dangerous_patterns = [
# Command chaining/injection
';', '&&', '||', '|',
# Shell expansion
'?', '[', ']', '{', '}',
# Command substitution
'`', '$(',
# IO redirection
'>', '>>', '<', '<<',
# Special characters
'\n', '\r', '\t',
# Path traversal
'..',
# Environment variables
'$', '%',
# Quotes that might be used for injection
'"', "'"
]
# First check: Command must match the allowed pattern
if ($normalized_cmd | find -r $allowed_regex | is-empty) {
print $'ERROR: Invalid git command format. (ansi r)Only simple `git show` or `git diff` commands are allowed(ansi reset).'
return false
}
# Second check: No dangerous patterns allowed
for pattern in $dangerous_patterns {
if ($cmd | str contains $pattern) {
print $'(ansi r)ERROR: Dangerous pattern detected: `($pattern)`(ansi reset)'
return false
}
}
# Third check: Command parts validation (increased limit to accommodate path patterns)
let cmd_parts = $normalized_cmd | split row ' '
if ($cmd_parts | length) > 6 {
print $'ERROR: Command too complex. (ansi r)Only simple `git show` or `git diff` commands are allowed(ansi reset).'
return false
}
true
}
alias main = deepseek-review