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

1 Commits

Author SHA1 Message Date
hustcer
adf7e63560 chore: Update logo 2025-02-16 23:19:47 +08:00
17 changed files with 191 additions and 585 deletions

View File

@@ -25,43 +25,43 @@ jobs:
uses: hustcer/deepseek-review@develop
with:
max-length: 50000
# 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
# 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: >
As a senior Nushell engineer, perform comprehensive script review with focus on:
As a senior DevOps engineer, perform comprehensive review of shell scripts with focus on:
### 1. Core Requirements:
- Validate Nu 0.90+ compatibility
- Check structured data handling
- Verify pipeline efficiency
- Assess module organization
1. Core Requirements:
- Validate POSIX compatibility
- Check for proper error handling
- Verify safe variable usage
- Assess resource management
### 2. Security Analysis:
- Command injection prevention
- Data leakage prevention
- Safe external command usage
- Proper permission validation
2. Security Analysis:
- Shell injection prevention
- Safe file operations
- Proper permissions handling
- Secure command execution
### 3. Performance Optimization:
- Pipeline optimization
- Memory usage patterns
- Builtin vs external command usage
- Parallel execution opportunities
3. Performance Optimization:
- Efficient process management
- Proper use of subshells
- Stream handling best practices
- Avoidance of unnecessary forks
**Rules:**
- Target Nu 0.90+ features
- Highlight data flow vulnerabilities
- Suggest structured data optimizations
- Keep feedback Nu-specific
- Use modern shell terminology
Rules:
- Target bash/sh compatibility
- Highlight security vulnerabilities
- Suggest performance improvements
- Keep feedback actionable
- Use technical shell terminology
**Required output structure:**
Required output structure:
#### Script Analysis
- Key observations
@@ -73,18 +73,19 @@ jobs:
**Overall Quality:** Rating (1-5)
Use the following reference data:
```yaml
checklist:
- Compatibility: ["Nu version", "Cross-platform support", "Plugin dependencies"]
- Security: ["Input sanitization", "Temporary file handling", "Env exposure"]
- Reliability: ["Error propagation", "Null handling", "Type validation"]
- Performance: ["Lazy evaluation", "Batch processing", "Stream handling"]
- Compatibility: ["POSIX compliance", "Shell-specific features", "Portability"]
- Security: ["Input validation", "Safe eval usage", "Permission checks"]
- Reliability: ["Error handling", "Exit codes", "Signal trapping"]
- Performance: ["Process management", "I/O operations", "Subshell usage"]
examples:
- issue: "❗ Unfiltered external command arguments in line 15 (command injection risk)"
- issue: "⚠️ Plaintext credentials in environment variables"
- suggestion: "Replace `each { }` with `par-each` for parallel processing"
- suggestion: "Use builtin `from json` instead of jq for better performance"
- issue: "❗ Unquoted variable expansion in line 42 (shell injection risk)"
- issue: "⚠️ Missing error handling for rm operation in line 15"
- suggestion: "Replace backticks with $() for better readability and nesting"
- suggestion: "Use exec for file handling to reduce file descriptors"
response_template: |
#### Script Analysis

View File

@@ -1,7 +1,6 @@
# Description: This workflow runs tests for hustcer/deepseek-review.
# REF:
# - https://github.com/vyadh/nutest/blob/main/.github/workflows/tests.yaml
# - https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
name: Run Tests

View File

@@ -1,35 +1,6 @@
# Changelog
All notable changes to this project will be documented in this file.
## [1.15.0] - 2025-03-23
### Features
- Add example code review prompts for frontend, java and rust to `config.example.yml` (#138)
- Post a comment to the PR to notify the user when no `CHAT_TOKEN` is provided (#143)
- Add nushell version check and notify for update (#144)
- Add `--config` option to specify config file path for local code review (#146)
- Support local DeepSeek model running on Ollama (#152)
- Add repo of current directory code review support (#161)
### Miscellaneous Tasks
- Publish test summary (#133)
- Update tests status badge to README
- Update code review prompt for current nushell repo (#139)
- Use SiliconFlow's DeepSeek API
- Some code refactor (#149)
- Add `just test` task to run tests locally
### Refactor
- Simplify `is-safe-git` common util (#150)
- Enhance the glob pattern handling in `glob-to-regex` function (#151)
### Deps
- Upgrade `Nushell` to `v0.103.0`
## [1.12.0] - 2025-02-16
### Bug Fixes

BIN
CR.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -21,7 +21,6 @@ set dotenv-load := true
set positional-arguments := true
# Just commands aliases
alias t := test
alias cr := code-review
# Use `just --evaluate` to show env vars
@@ -49,10 +48,6 @@ code-review *OPTIONS:
@overlay use {{ join(DEEPSEEK_REVIEW_PATH, 'nu', 'review.nu') }}; \
deepseek-review {{OPTIONS}}
# Run the test cases locally by nutest
test:
@use $'($nu.default-config-dir)/lib/nutest' *; run-tests
# Plugins need to be registered only once after nu v0.61
_setup:
@register -e json {{ join(NU_DIR, _query_plugin) }}

View File

@@ -1,3 +1,7 @@
<p align="center">
<img src="./CR.jpg" width="90" height="60" alt="DeepSeek Code Review Logo" />
</p>
# DeepSeek Code Review
![Tests](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhustcer%2Fb99391ee59016b17d0befe3331387e89%2Fraw%2Ftest-summary.json&query=%24.total&label=Tests)
@@ -22,7 +26,6 @@
- Streaming Output Support for Local Code Review
- Review Remote GitHub PRs Directly from Your Local CLI
- Review Commit Changes with DeepSeek for Any Local Repository by CLI
- Support On-demand Changes Generation via `git show`/`git diff` Command for Further Code Review
- Cross-platform Compatibility: Designed to function seamlessly across all platforms capable of running [Nushell](https://github.com/nushell/nushell)
### Both GH Action & Local
@@ -175,7 +178,7 @@ Usage:
Flags:
-d, --debug: Debug mode
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review, or local repo path / alias
-n, --pr-number <string>: GitHub PR number
-k, --gh-token <string>: Your GitHub token, fallback to GITHUB_TOKEN env var
-t, --diff-to <string>: Diff to git REF
@@ -184,13 +187,11 @@ Flags:
-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
-b, --base-url <string>: DeepSeek API base URL, fallback to BASE_URL env var
-U, --chat-url <string>: DeepSeek Model chat full API URL, e.g. http://localhost:11535/api/chat
-s, --sys-prompt <string>: Default to $DEFAULT_OPTIONS.SYS_PROMPT,
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
-i, --include <string>: Comma separated file patterns to include in the code review
-x, --exclude <string>: Comma separated file patterns to exclude in the code review
-T, --temperature <float>: Temperature for the model, between `0` and `2`, default value `1.0`
-C, --config <string>: Config file path, default to `config.yml`
-h, --help: Display the help message for this command
Parameters:
@@ -200,68 +201,34 @@ Parameters:
### Environment Configuration
To perform code review locally, you need to modify the configuration file. The repository already provides a configuration example [`config.example.yml`](https://github.com/hustcer/deepseek-review/blob/main/config.example.yml). Copy it to `config.yml` and modify it according to your actual needs. **Read the comments in the configuration file carefully**, as they explain the purpose of each configuration item.
To perform code reviews locally, you need to modify the configuration file. A sample configuration file [`config.example.yml`](https://github.com/hustcer/deepseek-review/blob/main/config.example.yml) is already provided in the repository. Copy it to `config.yml` and adjust it according to your actual setup.
> [!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
> Workflow. Please securely store any sensitive information in it and avoid committing it
> to the code repository.
**Create Command Alias**
For convenience in performing code review across any local repository, create a command alias. For example:
```sh
# For Nushell: Modify config.nu and add:
alias cr = nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml
# For zsh/bash: Modify ~/.zshrc or ~/.bashrc and add:
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
```
### Review Local Repository
To review a local repository:
- Navigate to the Git repository directory.
- Use the `cr` command to review current modifications, provided that `config.yml` is correctly configured.
**Usage Examples**
```sh
# Perform code review on the `git diff` changes in current directory
cr
# Perform code review on the `git diff f536acc` changes in current directory
cr --diff-from f536acc
# Perform code review on the `git diff f536acc 0dd0eb5` changes in current directory
cr --diff-from f536acc --diff-to 0dd0eb5
# Review the changes in current directory using the `--patch-cmd` flag
cr --patch-cmd 'git diff head~3'
cr -c 'git show head~3'
cr -c 'git diff 2393375 71f5a31'
cr -c 'git diff 2393375 71f5a31 nu/*'
cr -c 'git diff 2393375 71f5a31 :!nu/*'
# Dangerous commands like `cr -c 'git show head~3; rm ./*'` will not be allowed
```
### Review Remote GitHub PR Locally
When reviewing a remote GitHub PR locally:
- Always specify the PR number via `--pr-number`
- Use `--repo` to indicate the target repository (e.g., `hustcer/deepseek-review`). If `--repo` is omitted, the tool reads `settings.default-github-repo` from `config.yml`.
**Usage Examples**
### Usage Examples
```sh
# Perform code review on the `git diff` changes in the local DEFAULT_LOCAL_REPO repo
nu cr
# Perform code review on the `git diff f536acc` changes in the local DEFAULT_LOCAL_REPO repo
nu cr --diff-from f536acc
# Perform code review on the `git diff f536acc 0dd0eb5` changes in the local DEFAULT_LOCAL_REPO repo
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
cr --pr-number 31
nu cr --pr-number 31
# Perform code review on PR #31 in the remote hustcer/deepseek-review repo
cr --pr-number 31 --repo hustcer/deepseek-review
# Perform code review on PR #31 and exclude changes of pnpm-lock.yaml
cr --pr-number 31 --exclude pnpm-lock.yaml
nu cr --pr-number 31 --repo hustcer/deepseek-review
```
## License

View File

@@ -19,8 +19,7 @@
- 本地代码审查的时候支持流式输出
- 通过本地 CLI 直接审查远程 GitHub PR
- 通过本地 CLI 使用 DeepSeek 审查任何本地仓库的提交变更
- 允许通过自定义 `git show`/`git diff` 命令生成变更记录并进行审查
- 通过本地 CLI 使用 DeepSeek 分析任何本地仓库的提交变更
- 跨平台:理论上只要能运行 [Nushell](https://github.com/nushell/nushell) 即可使用本工具
### 本地或 GH Action
@@ -172,7 +171,7 @@ Usage:
Flags:
-d, --debug: Debug mode
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review
-r, --repo <string>: GitHub repo name, e.g. hustcer/deepseek-review, or local repo path / alias
-n, --pr-number <string>: GitHub PR number
-k, --gh-token <string>: Your GitHub token, fallback to GITHUB_TOKEN env var
-t, --diff-to <string>: Diff to git REF
@@ -181,13 +180,11 @@ Flags:
-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
-b, --base-url <string>: DeepSeek API base URL, fallback to BASE_URL env var
-U, --chat-url <string>: DeepSeek Model chat full API URL, e.g. http://localhost:11535/api/chat
-s, --sys-prompt <string>: Default to $DEFAULT_OPTIONS.SYS_PROMPT,
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
-i, --include <string>: Comma separated file patterns to include in the code review
-x, --exclude <string>: Comma separated file patterns to exclude in the code review
-T, --temperature <float>: Temperature for the model, between `0` and `2`, default value `1.0`
-C, --config <string>: Config file path, default to `config.yml`
-h, --help: Display the help message for this command
Parameters:
@@ -197,63 +194,33 @@ Parameters:
### 环境配置
在本地进行代码审查需要先修改配置文件,仓库里已经有了 [`config.example.yml`](https://github.com/hustcer/deepseek-review/blob/main/config.example.yml) 配置文件示例,将其拷贝到 `config.yml` 然后根据自己的实际情况进行修改即可,在修改配置文件的过程中请仔细阅读其中的注释,注释会说明每个配置项的作用
在本地进行代码审查需要先修改配置文件,仓库里已经有了 [`config.example.yml`](https://github.com/hustcer/deepseek-review/blob/main/config.example.yml) 配置文件示例,将其拷贝到 `config.yml` 然后根据自己的实际情况进行修改即可。
> [!WARNING]
>
> `config.yml` 配置文件仅在本地使用,在 GitHub Workflow 里面不会使用,里面的敏感信息请
> 妥善保存,不要提交到代码仓库里面
>
**创建命令别名**
为了方便您可以在任意本地仓库进行代码审查需要创建一个别名,比如:
```sh
# Nushell: 修改其 config.nu 配置文件,添加:
alias cr = nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml
# 对于 zsh 或 bash分别修改 ~/.zshrc or ~/.bashrc and add:
alias cr="nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml"
# After sourcing the profile you have edit, you can use `cr` now
```
之后就可以通过 `cr` 命令来进行代码审查了。
### 审查本地仓库
对本地仓库进行代码审查时需要先切换到 Git 仓库所在目录,然后通过 `cr` 命令即可对当前目录的当前修改进行代码审查,前提是您已经对 `config.yml` 进行了正确的配置。
**使用举例**
```sh
# 对本地当前目录所在仓库 `git diff` 修改内容进行代码审查
cr
# 对本地当前目录所在仓库 `git diff f536acc` 修改内容进行代码审查
cr --diff-from f536acc
# 对本地当前目录所在仓库 `git diff f536acc 0dd0eb5` 修改内容进行代码审查
cr --diff-from f536acc --diff-to 0dd0eb5
# 通过 --patch-cmd 参数对本地当前目录所在仓库变更内容进行审查
cr --patch-cmd 'git diff head~3'
cr -c 'git show head~3'
cr -c 'git diff 2393375 71f5a31'
cr -c 'git diff 2393375 71f5a31 nu/*'
cr -c 'git diff 2393375 71f5a31 :!nu/*'
# 像 `cr -c 'git show head~3; rm ./*'` 这样危险的命令将会被禁止
```
### 本地审查远程 GitHub PR
在本地对远程 GitHub 仓库的 PR 进行审查的时候一定要通过 `--pr-number` 传入待审查的 PR 编号,以及 `--repo` 指明待审查的仓库,比如 `hustcer/deepseek-review`, 如果没有指定 `--repo` 参数则从 config.yml 里面的 `settings.default-github-repo` 配置项读取待审查的仓库。
**使用举例**
### 使用举例
```sh
# 对本地 DEFAULT_LOCAL_REPO 仓库 `git diff` 修改内容进行代码审查
nu cr
# 对本地 DEFAULT_LOCAL_REPO 仓库 `git diff f536acc` 修改内容进行代码审查
nu cr --diff-from f536acc
# 对本地 DEFAULT_LOCAL_REPO 仓库 `git diff f536acc 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 进行代码审查
cr --pr-number 31
nu cr --pr-number 31
# 对远程 hustcer/deepseek-review 仓库编号为 31 的 PR 进行代码审查
cr --pr-number 31 --repo hustcer/deepseek-review
# 对 PR 进行审查的时候排除 pnpm-lock.yaml 文件的变更
cr --pr-number 31 --exclude pnpm-lock.yaml
nu cr --pr-number 31 --repo hustcer/deepseek-review
```
## 许可

View File

@@ -60,7 +60,7 @@ runs:
- name: Setup Nu
uses: hustcer/setup-nu@v3
with:
version: 0.103.0
version: 0.102.0
- name: DeepSeek Code Review
shell: nu {0}

View File

@@ -27,6 +27,8 @@ settings:
# This token is used to fetch the PR changes from GitHub API
# Default value will be ${{ github.token }} if used in GitHub Actions
github-token: 'YOUR_GITHUB_TOKEN'
# Default local repository to review, could be overrode by '-r' or '--repo'
default-local-repo: 'review'
# Default GitHub repository to review, could be overrode by '-r' or '--repo' if used with `-n` or `--pr-number`
default-github-repo: 'hustcer/deepseek-review'
# Include changes in the following file patterns
@@ -37,14 +39,6 @@ settings:
# Multiple providers could be defined, select the one by name in 'settings.provider'
# This way you could switch between different predefined providers easily
providers:
- name: ollama-local
token: 'empty'
chat-url: http://localhost:11555/api/chat
models:
- name: deepseek-r1
alias: r1
enabled: true
description: 'DeepSeek R1 model running on Ollama'
- name: 'DeepSeek'
token: 'YOUR_DEEPSEEK_TOKEN' # Required, The API token for the provider
base-url: 'https://api.deepseek.com'
@@ -70,6 +64,17 @@ providers:
alias: r1
description: 'SiliconFlow DeepSeek R1 model'
# Multiple local repositories could be defined, select the one by name in 'settings.default-local-repo'
# You can also use `-r` or `--repo` to specify the local repository to review by name to override the default
local-repos:
- name: 'review'
path: '/Users/hustcer/deepseek-review'
- name: 'milestone'
path: '/Users/hustcer/milestone-action'
- name: 'setup-nu'
path: '/Users/hustcer/setup-nu'
# Multiple Prompts could be defined, select the one by name in 'settings.user-prompt' or 'settings.system-prompt'
prompts:
user:
@@ -82,219 +87,6 @@ prompts:
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.
- name: frontend
prompt: >
As a senior Frontend engineer, perform comprehensive code review with focus on:
### 1. Core Requirements:
- Validate ES specification compliance
- Check component design patterns
- Verify state management efficiency
- Assess accessibility implementation
### 2. Security Analysis:
- XSS prevention measures
- CSRF protection implementation
- Safe third-party dependency usage
- Sensitive data handling practices
### 3. Performance Optimization:
- Resource loading strategies
- Rendering performance optimization
- Memory leak prevention
- Bundle size management
**Rules:**
- Target React/Vue framework conventions
- Highlight security vulnerabilities
- Suggest performance metrics improvements
- Provide code snippets for fixes
- Use modern frontend terminology
**Required output structure:**
#### Code Analysis
- Key observations
#### Security Review
- Vulnerability findings
#### Optimization Suggestions
- Performance improvements
**Overall Quality:** Rating (1-5)
```yaml
checklist:
- Compatibility: ["Browser support", "Responsive design", "ES specification"]
- Security: ["Input sanitization", "CORS configuration", "Cookie flags"]
- Reliability: ["Error boundaries", "Loading states", "Fallback UI"]
- Performance: ["Lazy loading", "Code splitting", "Rendering optimization"]
examples:
- issue: "❗ Unsanitized HTML insertion in line 28 (XSS risk)"
- issue: "⚠️ Missing `rel='noreferrer'` on external links in line 15"
- suggestion: "Implement virtual scrolling for large datasets in TableComponent"
- suggestion: "Convert CSS-in-JS to CSS Modules for better tree-shaking"
response_template: |
#### Code Analysis
- {{observations}}
{{#security_issues}}
#### Security Review
- {{security_issues}}
{{/security_issues}}
{{#optimizations}}
#### Optimization Suggestions
- {{optimizations}}
{{/optimizations}}
**Overall Quality:** {{rating}}
```
- name: java
prompt: >
As a senior Java Backend engineer, perform comprehensive code review with focus on:
### 1. Core Requirements:
- Validate Java coding standards compliance
- Check enterprise design patterns implementation
- Verify resource management efficiency
- Assess concurrency control mechanisms
### 2. Security Analysis:
- SQL/NoSQL injection prevention
- Sensitive data encryption handling
- Proper authentication/authorization
- Secure session management
### 3. Performance Optimization:
- Thread pool configuration analysis
- Database connection management
- Cache strategy evaluation
- GC tuning opportunities
**Rules:**
- Target Spring Boot/Jakarta EE frameworks
- Highlight OWASP TOP 10 vulnerabilities
- Suggest JVM optimization strategies
- Provide code snippets for fixes
- Use enterprise Java terminology
**Required output structure:**
#### Code Analysis
- Key observations
#### Security Review
- Vulnerability findings
#### Optimization Suggestions
- Performance improvements
**Overall Quality:** Rating (1-5)
```yaml
checklist:
- Compatibility: ["JDK version", "Framework version", "API contracts"]
- Security: ["Input validation", "CSRF protection", "Security headers"]
- Reliability: ["Exception handling", "Transaction management", "Circuit breakers"]
- Performance: ["Connection pooling", "Query optimization", "Object reuse"]
examples:
- issue: "❗ Unvalidated user input in DAO layer (SQL injection risk)[2,3](@ref)"
- issue: "⚠️ Plaintext credentials storage in properties file[2](@ref)"
- suggestion: "Replace synchronized blocks with ReentrantLock for better concurrency[3](@ref)"
- suggestion: "Implement prepared statement caching in JDBC configuration[3](@ref)"
response_template: |
#### Code Analysis
- {{observations}}
{{#security_issues}}
#### Security Review
- {{security_issues}}
{{/security_issues}}
{{#optimizations}}
#### Optimization Suggestions
- {{optimizations}}
{{/optimizations}}
**Overall Quality:** {{rating}}
```
- name: rust
prompt: >
As a senior Rust engineer, perform comprehensive code review with focus on:
### 1. Core Requirements:
- Validate Rust coding standards compliance
- Check memory safety guarantees
- Verify concurrency model correctness
- Assess error handling patterns
### 2. Security Analysis:
- Unsafe code usage validation
- Data race prevention measures
- Input sanitization practices
- Supply chain security checks
### 3. Performance Optimization:
- Memory allocation patterns
- Iterator vs loop efficiency
- Parallel execution opportunities
- Zero-cost abstraction utilization
**Rules:**
- Target Rust 2021 edition features
- Highlight memory safety violations
- Suggest lifetime optimization strategies
- Provide unsafe code alternatives
- Use Rust ecosystem terminology
**Required output structure:**
#### Code Analysis
- Key observations
#### Security Review
- Vulnerability findings
#### Optimization Suggestions
- Performance improvements
**Overall Quality:** Rating (1-5)
```yaml
checklist:
- Compatibility: ["Edition compliance", "Crate versioning", "FFI safety"]
- Security: ["Unsafe scoping", "Panic safety", "Trait bounds"]
- Reliability: ["Error propagation", "Test coverage", "Documentation"]
- Performance: ["Allocation tracking", "Concurrency patterns", "SIMD usage"]
examples:
- issue: "❗ Unmarked unsafe block in line 75 (memory safety violation risk)[1](@ref)"
- issue: "⚠️ Missing error handling for Result in line 42[1](@ref)"
- suggestion: "Replace Box<dyn Trait> with impl Trait for better monomorphization[1](@ref)"
- suggestion: "Use crossbeam-channel instead of std::sync::mpsc for improved throughput[3](@ref)"
response_template: |
#### Code Analysis
- {{observations}}
{{#security_issues}}
#### Security Review
- {{security_issues}}
{{/security_issues}}
{{#optimizations}}
#### Optimization Suggestions
- {{optimizations}}
{{/optimizations}}
**Overall Quality:** {{rating}}
```
- name: strict-dev
prompt: >
Act as a senior engineer performing rigorous code review. Analyze the provided git diff output through

14
cr
View File

@@ -4,14 +4,14 @@
# Description: A wrapper for nu/review.nu as the main entry point of the project.
use nu/config.nu *
use nu/common.nu [hr-line, check-nushell, ECODE]
use nu/common.nu [hr-line, ECODE]
use nu/review.nu [deepseek-review]
# Use DeepSeek AI to review code changes locally or in GitHub Actions
def main [
token?: string, # Your DeepSeek API token, fallback to CHAT_TOKEN env var
--debug(-d), # Debug mode
--repo(-r): string, # GitHub repo name, e.g. hustcer/deepseek-review
--repo(-r): string, # GitHub repo name, e.g. hustcer/deepseek-review, or local repo path / alias
--pr-number(-n): string, # GitHub PR number
--gh-token(-k): string, # Your GitHub token, fallback to GITHUB_TOKEN env var
--diff-to(-t): string, # Diff to git REF
@@ -20,18 +20,15 @@ def main [
--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
--chat-url(-U): string, # DeepSeek Model chat full API URL, e.g. http://localhost:11535/api/chat
--sys-prompt(-s): string # Default to $DEFAULT_OPTIONS.SYS_PROMPT,
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
--include(-i): string, # Comma separated file patterns to include in the code review
--exclude(-x): string, # Comma separated file patterns to exclude in the code review
--temperature(-T): float, # Temperature for the model, between `0` and `2`, default value `1.0`
--config(-C): string # Config file path, default to `config.yml`
--temperature(-T): float, # Temperature for the model, between `0` and `2`, default value `1.0`, Only for V3
] {
check-nushell
config-check --config=$config
config-load --debug=$debug --config=$config --model=$model
config-check
config-load --debug=$debug --repo=$repo --model=$model
(
deepseek-review $token
--repo=$repo
@@ -40,7 +37,6 @@ def main [
--exclude=$exclude
--model=$env.CHAT_MODEL
--base-url=$base_url
--chat-url=$chat_url
--gh-token=$gh_token
--diff-to=$diff_to
--diff-from=$diff_from

View File

@@ -8,15 +8,12 @@ words:
- MPFR
- nuon
- pwsh
- JDBC
- mpsc
- nuons
- ECODE
- vyadh
- nutest
- endfor
- dotenv
- Ollama
- hustcer
- Nushell
- creatio
@@ -29,9 +26,4 @@ words:
- Infinigence
- SILICONFLOW
- USERPROFILE
- noreferrer
- Unsanitized
- Unvalidated
- monomorphization
ignorePaths:
- config.yml

View File

@@ -1,7 +1,7 @@
{
"name": "deepseek-review",
"version": "1.15.0",
"actionVer": "v1.15",
"version": "1.12.0",
"actionVer": "v1.12",
"author": "hustcer",
"license": "MIT",
"github": "https://github.com/hustcer/deepseek-review",

View File

@@ -4,8 +4,6 @@
# Description: Common helpers for DeepSeek-Review
#
use kv.nu ['kv set', 'kv get']
# Commonly used exit codes
export const ECODE = {
SUCCESS: 0,
@@ -57,27 +55,6 @@ export def compare-ver [v1: string, v2: string] {
0
}
# Check nushell version and notify user to upgrade it if outdated
# Check version once daily and cache the result
export def check-nushell [--debug] {
const _DATE_FMT = '%Y.%m.%d'
const V_KEY = 'NU-VERSION-CHECK@DEEPSEEK-REVIEW'
let version_cached = kv get -u $V_KEY
let today = date now | format date $_DATE_FMT
let check = if ($version_cached | is-empty) or $version_cached.date? != $today {
let $check = try { ({ ...(version check), date: $today }) } catch { ({ current: true }) }
if $debug { print 'Checking for the latest Nushell version...'; $check | print }
kv set -u $V_KEY $check --return value
} else {
$version_cached
}
# If the current version is the latest after user upgrade, return
if $check.current or (compare-ver $check.latest (version).version) == 0 { return }
print $'(char nl) (ansi yr) WARNING: (ansi reset) Your Nushell is (ansi r)OUTDATED(ansi reset)'
print $' ------------> Please upgrade Nushell to the latest version: (ansi g)($check.latest)(ansi reset) <------------'
print -n (char nl)
}
# Converts a .env file into a record
# may be used like this: open .env | load-env
# works with quoted and unquoted .env files
@@ -166,9 +143,3 @@ export def has-ref [
let parse = (do -i { git rev-parse --verify -q $ref } | complete)
if ($parse.stdout | is-empty) { false } else { true }
}
# Notify the user that the `CHAT_TOKEN` hasn't been configured
export const NO_TOKEN_TIP = (
"**Notice:** It looks like you're using [`hustcer/deepseek-review`](https://github.com/hustcer/deepseek-review), but the `CHAT_TOKEN` hasn't" +
"been configured in your repo's Variables/Secrets. Please ensure this token is set for proper functionality. For step-by-step guidance, refer" +
"to the **CHAT_TOKEN Config** section of [README](https://github.com/hustcer/deepseek-review/blob/main/README.md#code-review-with-github-action).")

View File

@@ -91,9 +91,9 @@ def check-models [options: record] {
}
# Check if the config.yml file exists and if it's valid
export def config-check [--config: string = $SETTING_FILE] {
file-exists $config
let options = open $config
export def config-check [] {
file-exists $SETTING_FILE
let options = open $SETTING_FILE
check-prompts $options
check-providers $options
check-models $options
@@ -117,17 +117,22 @@ def get-model-envs [settings: record, model?: string = ''] {
| get -i 0.name
| 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_MODEL: $model_name }
}
# Load the config.yml file to the environment
export def --env config-load [
--debug(-d), # Print the loaded environment variables
--config(-C): string, # The config file path, default to `config.yml`
--repo(-r): string, # Load the specified local repository by name
--model(-m): string, # Load the specified model by name
] {
let all_settings = open ($config | default $SETTING_FILE)
let all_settings = open $SETTING_FILE
let settings = $all_settings | get settings? | default {}
let local_repo = $all_settings.local-repos
| default []
| where name == ($repo | default $settings.default-local-repo? | default '')
| get -i 0.path
| default $repo
let user_prompt = $all_settings.prompts?.user?
| default []
@@ -150,6 +155,7 @@ export def --env config-load [
GITHUB_TOKEN: $settings.github-token,
EXCLUDE_PATTERNS: $settings.exclude-patterns,
INCLUDE_PATTERNS: $settings.include-patterns,
DEFAULT_LOCAL_REPO: $local_repo,
DEFAULT_GITHUB_REPO: $settings.default-github-repo,
}
load-env $env_vars

View File

@@ -27,7 +27,7 @@
use kv.nu *
use common.nu [
ECODE, NO_TOKEN_TIP, hr-line, is-installed, windows?, mac?,
ECODE, hr-line, is-installed, windows?, mac?,
compare-ver, compact-record, git-check, has-ref,
]
@@ -63,7 +63,6 @@ export def --env deepseek-review [
--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
--chat-url(-U): string, # DeepSeek Model chat full API URL, e.g. http://localhost:11535/api/chat
--sys-prompt(-s): string # Default to $DEFAULT_OPTIONS.SYS_PROMPT,
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
--include(-i): string, # Comma separated file patterns to include in the code review
@@ -72,18 +71,21 @@ export def --env deepseek-review [
]: nothing -> nothing {
$env.config.table.mode = 'psql'
let local_repo = $env.PWD
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 base_url = $base_url | default $env.BASE_URL? | default $DEFAULT_OPTIONS.BASE_URL
let url = $chat_url | default $env.CHAT_URL? | default $'($base_url)/chat/completions'
let max_length = try { $max_length | default ($env.MAX_LENGTH? | default 0 | into int) } catch { 0 }
let temperature = try { $temperature | default $env.TEMPERATURE? | default $DEFAULT_OPTIONS.TEMPERATURE | into float } catch { $DEFAULT_OPTIONS.TEMPERATURE }
validate-temperature $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,
model: $model,
@@ -100,7 +102,10 @@ export def --env deepseek-review [
}
$env.GH_TOKEN = $gh_token | default $env.GITHUB_TOKEN?
validate-token $token --pr-number $pr_number --repo $repo
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
}
let hint = if not $is_action and ($pr_number | is-empty) {
$'🚀 Initiate the code review by DeepSeek AI for local changes ...'
} else {
@@ -133,12 +138,12 @@ export def --env deepseek-review [
]
}
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)($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 ($url) ...(ansi reset)'
print $'(ansi r)Oops, No response returned from ($base_url) ...(ansi reset)'
exit $ECODE.SERVER_ERROR
}
if $debug { print $'DeepSeek Model Response:'; hr-line; $response | table -e | print }
@@ -147,7 +152,7 @@ export def --env deepseek-review [
exit $ECODE.SERVER_ERROR
}
let reason = $response | get -i choices.0.message.reasoning_content
let review = $response | get -i choices.0.message.content | default ($response | get -i message.content)
let 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) ...'
@@ -157,49 +162,12 @@ export def --env deepseek-review [
if not $is_action {
print $'Code Review Result:'; hr-line; print $result
} else {
post-comments-to-pr $repo $pr_number $result
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json ...$HTTP_HEADERS]
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.'
}
if ($response.usage? | is-not-empty) {
print $'(char nl)Token Usage:'; hr-line
$response.usage? | table -e | print
}
}
# Validate the DeepSeek API token
def validate-token [token?: string, --pr-number: string, --repo: string] {
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)'
if ($pr_number | is-not-empty) { post-comments-to-pr $repo $pr_number $NO_TOKEN_TIP }
exit $ECODE.INVALID_PARAMETER
}
$token
}
# Validate the temperature value
def validate-temperature [temp: float] {
if ($temp < 0) or ($temp > 2) {
print $'(ansi r)Invalid temperature value, should be in the range of 0 to 2.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
$temp
}
# Post review comments to GitHub PR
def post-comments-to-pr [
repo: string, # GitHub repository name, e.g. hustcer/deepseek-review
pr_number: string, # GitHub PR number
comments: string, # Comments content to post
] {
let comment_url = $'($GITHUB_API_BASE)/repos/($repo)/issues/($pr_number)/comments'
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json ...$HTTP_HEADERS]
try {
http post -t application/json -H $BASE_HEADER $comment_url { body: $comments }
} catch {|err|
print $'(ansi r)Failed to post comments to PR: (ansi reset)'
$err | table -e | print
exit $ECODE.SERVER_ERROR
}
print $'(char nl)Token Usage:'; hr-line
$response.usage | table -e | print
}
# Output the streaming response of review result from DeepSeek API
@@ -217,7 +185,7 @@ def streaming-output [
let res = $in
let type = $res | describe
let record_error = $type =~ '^record'
let other_error = $type =~ '^string' and $res !~ 'data: ' and $res !~ 'done'
let other_error = $type =~ '^string' and $res !~ 'data: '
if $record_error or $other_error {
$res | table -e | print
exit $ECODE.SERVER_ERROR
@@ -227,11 +195,10 @@ def streaming-output [
| each {|line|
if $line == $RESPONSE_END { return }
if ($line | is-empty) { return }
# DeepSeek Response vs Local Ollama Response
let $last = if $line =~ '^data: ' { $line | str substring 6.. | from json } else { $line | from json }
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 | default ($last | get -i message) | if ($in | is-not-empty) {
$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 }
@@ -257,9 +224,16 @@ export def get-diff [
--exclude: string, # Comma separated file patterns to exclude in the code review
--patch-cmd: string, # The `git show` or `git diff` command to get the diff content
] {
let local_repo = $env.PWD
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json]
let DIFF_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3.diff]
let local_repo = $env.DEFAULT_LOCAL_REPO? | default (pwd)
if ($pr_number | is-empty) {
if not ($local_repo | path exists) {
print $'(ansi r)The directory ($local_repo) does not exist.(ansi reset)'
exit $ECODE.CONDITION_NOT_SATISFIED
}
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)'
@@ -361,36 +335,11 @@ export def prepare-awk [] {
# 2. Convert ? to . (optional, as needed)
# 3. Convert / to \/
def glob-to-regex [patterns: list<string>] {
# Handle empty patterns list
if ($patterns | length) == 0 { return '' }
# Define a mapping of characters to escape
let regex_escapes = {
# Escape special regex characters first
"\\.": "\\\\.",
"\\+": "\\\\+",
"\\^": "\\\\^",
"\\$": "\\\\$",
"\\(": "\\\\(",
"\\)": "\\\\)",
"\\[": "\\\\[",
"\\]": "\\\\]",
"\\{": "\\\\{",
"\\}": "\\\\}",
"\\|": "\\\\|",
# Then convert glob patterns to regex patterns
"*": ".*",
"?": ".",
"/": "\\/",
}
$patterns
| each { |pat|
$regex_escapes | columns | reduce -f $pat { |k, acc|
$acc | str replace -a $k ($regex_escapes | get $k)
}
$pat | str replace "*" ".*" | str replace "?" "." | str replace "/" "\\/"
}
| str join '|'
| str join "|"
}
# Generate the awk include regex pattern string for the specified patterns
@@ -410,17 +359,57 @@ export def generate-exclude-regex [patterns: list<string>] {
# - 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)
# 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}$'
# 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_\-\.\*\/]+)?$'
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)'
# 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

View File

@@ -1,10 +1,7 @@
use std/assert
use ../nu/common.nu [
compare-ver, 'from env', is-installed, has-ref,
git-check, compact-record, is-repo, windows?, mac?,
]
use ../nu/common.nu [compare-ver, 'from env', is-installed, has-ref, git-check, compact-record]
#[test]
def 'compare-verv1.0.0 is greater than v0.999.0' [] {
@@ -55,11 +52,6 @@ def 'has-refgit repo should has HEAD ref' [] {
assert equal (has-ref 0000) false
}
#[test]
def 'is-repocurrent dir is a git repo' [] {
assert equal (is-repo) true
}
#[test]
def 'git-checkcurrent dir is a git repo' [] {
assert equal (git-check (pwd) --check-repo=1) true
@@ -70,31 +62,3 @@ def 'compact-recordshould work as expected' [] {
assert equal ({a: null, b: '', c: 'abc' } | compact-record) { c: 'abc' }
assert equal ({a: null, b: 0, c: 1, e: { f: 'g' } } | compact-record) { b: 0, c: 1, e: { f: 'g' } }
}
#[test]
def 'OS check should work as expected' [] {
# `$env.RUNNER_OS` Possible values are Linux, Windows, or macOS in GitHub Actions
match $nu.os-info.name {
'windows' => {
assert equal (mac?) false
assert equal (windows?) true
if ($env.RUNNER_OS? | is-not-empty) {
assert equal $env.RUNNER_OS Windows
}
}
'macos' => {
assert equal (mac?) true
assert equal (windows?) false
if ($env.RUNNER_OS? | is-not-empty) {
assert equal $env.RUNNER_OS macOS
}
}
_ => {
assert equal (mac?) false
assert equal (windows?) false
if ($env.RUNNER_OS? | is-not-empty) {
assert equal $env.RUNNER_OS Linux
}
}
}
}

View File

@@ -25,13 +25,9 @@ def 'is-safe-gitshould work as expected' [] {
assert equal (is-safe-git 'git checkout') false
assert equal (is-safe-git 'git show 0dd0eb5') true
assert equal (is-safe-git 'git show HEAD') true
assert equal (is-safe-git 'git show head~1') true
assert equal (is-safe-git 'git diff HEAD~2') true
assert equal (is-safe-git 'git diff head~3 main') true
assert equal (is-safe-git 'git diff f536acc 0dd0eb5') true
assert equal (is-safe-git 'git show 2393375 | less') false
assert equal (is-safe-git 'git show 2393375>diff.patch') false
assert equal (is-safe-git 'git show 2393375 o+e>diff.patch') false
assert equal (is-safe-git 'git diff f536acc 0dd0eb5 nu/*') true
assert equal (is-safe-git 'git diff f536acc 0dd0eb5 :!nu/*') true
assert equal (is-safe-git 'git diff f536acc 0dd0eb5 :!nu/*; rm -rf abc') false