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

8 Commits
v1.15 ... v1.16

Author SHA1 Message Date
hustcer
0679ef2b0e Bump to v1.16 2025-04-05 10:13:41 +08:00
hustcer
ac1bb26376 chore: Add openrouter.ai config example 2025-04-03 18:09:41 +08:00
Justin Ma
443d1d887e feat: Add openrouter deepseek model support (#167)
* feat: Add openrouter deepseek V3 model support

* feat: Add openrouter deepseek R1 model support

* feat: Add openrouter deepseek model support for GitHub action

* feat: Add openrouter deepseek model support for GitHub action

* feat: Add openrouter deepseek model support for GitHub action
2025-04-03 18:05:46 +08:00
Justin Ma
90d7be5ff2 refactor: Replace custom kv.nu module with std-rfc/kv for key-value functionality (#166)
* refactor: Replace custom `kv.nu` module with `std-rfc/kv` for key-value functionality

* refactor: Replace custom `kv.nu` module with `std-rfc/kv` for key-value functionality

* ci skip
2025-04-02 20:11:14 +08:00
Justin Ma
9af6f3d480 refactor: Refactored diff handling by moving logic to separate module (#165)
* refactor: Refactored diff handling by moving logic to separate module

* refactor: Refactored diff handling by moving logic to separate module

* Fix tests

* Fix tests
2025-04-02 19:54:45 +08:00
Justin Ma
abe4d66650 refactor: Refactor get-diff custom command (#164) 2025-04-02 18:20:14 +08:00
Justin Ma
da010fada9 doc: Add powershell alias setup guide (#163) 2025-03-25 15:45:23 +08:00
hustcer
61a7b5f654 chore: Add set fish alias guide 2025-03-24 20:24:08 +08:00
13 changed files with 384 additions and 405 deletions

View File

@@ -62,7 +62,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
use ${{ github.workspace }}/nutest/nutest
use ${{ github.workspace }}/nu/review.nu [prepare-awk]
use ${{ github.workspace }}/nu/util.nu [prepare-awk]
prepare-awk
(
nutest run-tests

View File

@@ -1,6 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.
## [1.16.0] - 2025-04-05
### Documentation
- Add alias setup guide for `powershell` (#163)
### Features
- Add OpenRouter deepseek model support (#167)
### Miscellaneous Tasks
- Add alias guide for `fish`
- Add openrouter.ai config example
- Set minimum required nushell version to v0.103
### Refactor
- Refactor `get-diff` custom command (#164)
- Refactor diff handling by moving logic to separate module (#165)
- Replace custom `kv.nu` module with `std-rfc/kv` for key-value functionality (#166)
## [1.15.0] - 2025-03-23
### Features

View File

@@ -163,7 +163,7 @@ 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). It is recommended to install the latest versions.
- [`Nushell`](https://www.nushell.sh/book/installation.html). It is recommended to install the latest versions(min version required: `0.103`).
- 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:
@@ -215,10 +215,17 @@ For convenience in performing code review across any local repository, create a
# For Nushell: Modify config.nu and add:
alias cr = nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml
# For zsh/bash: Modify ~/.zshrc or ~/.bashrc and add:
# Modify ~/.zshrc for zsh or ~/.bashrc for bash or ~/.config/fish/config.fish for fish and add:
alias cr="nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml"
# After sourcing the modified profile, use `cr` for code review
# For Windows powershell users please set cr alias by editing $PROFILE and add:
function cr {
nu D:\absolute\path\to\deepseek-review\cr --config D:\absolute\path\to\deepseek-review\config.yml @args
}
# Then restart the terminal or run `. $PROFILE` in pwsh to make `cr` work
```
### Review Local Repository

View File

@@ -160,7 +160,7 @@ DeepSeek 接口调用入参:
在本地进行代码审查,支持 `macOS`, `Ubuntu` & `Windows` 不过需要安装以下工具:
- [`Nushell`](https://www.nushell.sh/book/installation.html), 建议安装最新版本
- [`Nushell`](https://www.nushell.sh/book/installation.html), 建议安装最新版本(最低版本 `0.103`)
- [`awk`](https://github.com/onetrueawk/awk) 或者 [`gawk`](https://www.gnu.org/software/gawk/) 的最新版版本,优先推荐 `gawk`
- 接下来只需要把本仓库代码克隆到本地,然后进入仓库目录执行 `nu cr -h` 即可看到类似如下输出:
@@ -212,9 +212,17 @@ Parameters:
```sh
# Nushell: 修改其 config.nu 配置文件,添加:
alias cr = nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml
# 对于 zsh 或 bash分别修改 ~/.zshrc or ~/.bashrc and add:
# Modify ~/.zshrc for zsh or ~/.bashrc for bash or ~/.config/fish/config.fish for fish and add:
alias cr="nu /absolute/path/to/deepseek-review/cr --config /absolute/path/to/deepseek-review/config.yml"
# After sourcing the profile you have edit, you can use `cr` now
# After sourcing the profile you have edit, you can use `cr` now
# For Windows powershell users please set cr alias by editing $PROFILE and add:
function cr {
nu D:\absolute\path\to\deepseek-review\cr --config D:\absolute\path\to\deepseek-review\config.yml @args
}
# Then restart the terminal or run `. $PROFILE` in pwsh to make `cr` work
```
之后就可以通过 `cr` 命令来进行代码审查了。

View File

@@ -45,6 +45,7 @@ providers:
alias: r1
enabled: true
description: 'DeepSeek R1 model running on Ollama'
- name: 'DeepSeek'
token: 'YOUR_DEEPSEEK_TOKEN' # Required, The API token for the provider
base-url: 'https://api.deepseek.com'
@@ -70,6 +71,18 @@ providers:
alias: r1
description: 'SiliconFlow DeepSeek R1 model'
- name: OpenRouter
token: sk-or-v1-*****
base-url: https://openrouter.ai/api/v1
models:
- name: deepseek/deepseek-chat-v3-0324:free
alias: v3
enabled: true
description: 'OpenRouter DeepSeek V3 model'
- name: deepseek/deepseek-r1:free
alias: r1
description: 'OpenRouter DeepSeek R1 model'
# Multiple Prompts could be defined, select the one by name in 'settings.user-prompt' or 'settings.system-prompt'
prompts:
user:

View File

@@ -26,10 +26,11 @@ words:
- linewise
- Subshell
- subshells
- noreferrer
- OPENROUTER
- Infinigence
- SILICONFLOW
- USERPROFILE
- noreferrer
- Unsanitized
- Unvalidated
- monomorphization

View File

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

View File

@@ -4,7 +4,7 @@
# Description: Common helpers for DeepSeek-Review
#
use kv.nu ['kv set', 'kv get']
use std-rfc/kv ['kv set', 'kv get']
# Commonly used exit codes
export const ECODE = {
@@ -18,6 +18,8 @@ export const ECODE = {
CONDITION_NOT_SATISFIED: 8,
}
export const GITHUB_API_BASE = 'https://api.github.com'
# If current host is Windows
export def windows? [] {
# Windows / Darwin

150
nu/diff.nu Normal file
View File

@@ -0,0 +1,150 @@
#!/usr/bin/env nu
# Author: hustcer
# Created: 2025/04/02 20:02:15
# Description: Diff command for DeepSeek-Review
use common.nu [GITHUB_API_BASE, ECODE, git-check, has-ref]
use util.nu [generate-include-regex, generate-exclude-regex, prepare-awk]
# If the PR title or body contains any of these keywords, skip the review
const IGNORE_REVIEW_KEYWORDS = ['skip review' 'skip cr']
# Get the diff content from GitHub PR or local git changes and apply filters
export def get-diff [
--repo: string, # GitHub repository name
--pr-number: string, # GitHub PR number
--diff-to: string, # Diff to git ref
--diff-from: string, # Diff from git ref
--include: string, # Comma separated file patterns to include in the code review
--exclude: string, # Comma separated file patterns to exclude in the code review
--patch-cmd: string, # The `git show` or `git diff` command to get the diff content
] {
let content = (
get-diff-content --repo $repo --pr-number $pr_number --patch-cmd $patch_cmd
--diff-to $diff_to --diff-from $diff_from --include $include --exclude $exclude)
if ($content | is-empty) {
print $'(ansi g)Nothing to review.(ansi reset)'
exit $ECODE.SUCCESS
}
apply-file-filters $content --include $include --exclude $exclude
}
# Get diff content from GitHub PR or local git changes
def get-diff-content [
--repo: string, # GitHub repository name
--pr-number: string, # GitHub PR number
--diff-to: string, # Diff to git ref
--diff-from: string, # Diff from git ref
--include: string, # Comma separated file patterns to include in the code review
--exclude: string, # Comma separated file patterns to exclude in the code review
--patch-cmd: string, # The `git show` or `git diff` command to get the diff content
] {
let local_repo = $env.PWD
if ($pr_number | is-not-empty) {
get-pr-diff --repo $repo $pr_number
} else if ($diff_from | is-not-empty) {
get-ref-diff $diff_from --diff-to $diff_to
} else if not (git-check $local_repo --check-repo=1) {
print $'Current directory ($local_repo) is (ansi r)NOT(ansi reset) a git repo, bye...(char nl)'
exit $ECODE.CONDITION_NOT_SATISFIED
} else if ($patch_cmd | is-not-empty) {
get-patch-diff $patch_cmd
} else {
git diff
}
}
# Get the diff content of the specified GitHub PR,
# if the PR description contains the skip keyword, exit
def get-pr-diff [
--repo: string, # GitHub repository name
pr_number: string, # GitHub PR number
] {
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json]
let DIFF_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3.diff]
if ($repo | is-empty) {
print $'(ansi r)Please provide the GitHub repository name by `--repo` option.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
let description = http get -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)'
| select title body | values | str join "\n"
# Check if the PR title or body contains keywords to skip the review
if ($IGNORE_REVIEW_KEYWORDS | any {|it| $description =~ $it }) {
print $'(ansi r)The PR title or body contains keywords to skip the review, bye...(ansi reset)'
exit $ECODE.SUCCESS
}
# Get the diff content of the PR
http get -H $DIFF_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)' | str trim
}
# Get diff content from local git changes
def get-ref-diff [
diff_from: string, # Diff from git REF
--diff-to: string, # Diff to git ref
] {
# Validate the git refs
if not (has-ref $diff_from) {
print $'(ansi r)The specified git ref ($diff_from) does not exist, please check it again.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
if ($diff_to | is-not-empty) and not (has-ref $diff_to) {
print $'(ansi r)The specified git ref ($diff_to) does not exist, please check it again.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
git diff $diff_from ($diff_to | default HEAD)
}
# Get the diff content from the specified git command
def get-patch-diff [
cmd: string # The `git show` or `git diff` command to get the diff content
] {
let valid = is-safe-git $cmd
if not $valid {
exit $ECODE.INVALID_PARAMETER
}
# Get the diff content from the specified git command
nu -c $cmd
}
# Apply file filters to the diff content to include or exclude specific files
def apply-file-filters [
content: string, # The diff content to filter
--include: string, # Comma separated file patterns to include in the code review
--exclude: string, # Comma separated file patterns to exclude in the code review
] {
mut filtered_content = $content
let awk_bin = (prepare-awk)
let outdated_awk = $'If you are using an (ansi r)outdated awk version(ansi reset), please upgrade to the latest version or use gawk latest instead.'
if ($include | is-not-empty) {
let patterns = $include | split row ','
$filtered_content = $filtered_content | try {
^$awk_bin (generate-include-regex $patterns)
} catch {
print $outdated_awk
exit $ECODE.OUTDATED
}
}
if ($exclude | is-not-empty) {
let patterns = $exclude | split row ','
$filtered_content = $filtered_content | try {
^$awk_bin (generate-exclude-regex $patterns)
} catch {
print $outdated_awk
exit $ECODE.OUTDATED
}
}
$filtered_content
}

187
nu/kv.nu
View File

@@ -1,187 +0,0 @@
# NOTE: This file was copied from https://github.com/nushell/nushell/tree/main/crates/nu-std/std-rfc/kv
# And will be removed after the Nu v0.103 release
# More examples could be found here: https://github.com/nushell/nushell/discussions/14965
#
# kv module
#
# use std-rfc/kv *
#
# Easily store and retrieve key-value pairs
# in a pipeline.
#
# A common request is to be able to assign a
# pipeline result to a variable. While it's
# not currently possible to use a "let" statement
# within a pipeline, this module provides an
# alternative. Think of each key as a variable
# that can be set and retrieved.
# Stores the pipeline value for later use
#
# If the key already exists, it is updated to the new value provided.
export def "kv set" [
key: string
value_or_closure?: any
--return (-r): string # Whether and what to return to the pipeline output
--universal (-u)
] {
# Pipeline input is preferred, but prioritize
# parameter if present. This allows $in to be
# used in the parameter if needed.
let input = $in
# If passed a closure, execute it
let arg_type = ($value_or_closure | describe)
let value = match $arg_type {
closure => { $input | do $value_or_closure }
_ => ($value_or_closure | default $input)
}
# Store values as nuons for type-integrity
let kv_pair = {
session: '' # Placeholder
key: $key
value: ($value | to nuon)
}
let db_open = (db_setup --universal=$universal)
try {
# Delete the existing key if it does exist
do $db_open | query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $key }
}
match $universal {
true => { $kv_pair | into sqlite (universal_db_path) -t std_kv_store }
false => { $kv_pair | stor insert -t std_kv_store }
}
# The value that should be returned from `kv set`
# By default, this is the input to `kv set`, even if
# overridden by a positional parameter.
# This can also be:
# input: (Default) The pipeline input to `kv set`, even if
# overridden by a positional parameter. `null` if no
# pipeline input was used.
# ---
# value: If a positional parameter was used for the value, then
# return it, otherwise return the input (whatever was set).
# If the positional was a closure, return the result of the
# closure on the pipeline input.
# ---
# all: The entire contents of the existing kv table are returned
match ($return | default 'input') {
'all' => (kv list --universal=$universal)
'a' => (kv list --universal=$universal)
'value' => $value
'v' => $value
'input' => $input
'in' => $input
'i' => $input
_ => {
error make {
msg: "Invalid --return option"
label: {
text: "Must be 'all'/'a', 'value'/'v', or 'input'/'in'/'i'"
span: (metadata $return).span
}
}
}
}
}
# Retrieves a stored value by key
#
# Counterpart of "kv set". Returns null if the key is not found.
export def "kv get" [
key: string # Key of the kv-pair to retrieve
--universal (-u)
] {
let db_open = (db_setup --universal=$universal)
do $db_open
# Hack to turn a SQLiteDatabase into a table
| $in.std_kv_store | wrap temp | get temp
| where key == $key
# Should only be one occurrence of each key in the stor
| get -i value.0
| match $in {
# Key not found
null => null
# Key found
_ => { from nuon }
}
}
# List the currently stored key-value pairs
#
# Returns results as the Nushell value rather than the stored nuon.
export def "kv list" [
--universal (-u)
] {
let db_open = (db_setup --universal=$universal)
do $db_open | $in.std_kv_store? | each {|kv_pair|
{
key: $kv_pair.key
value: ($kv_pair.value | from nuon )
}
}
}
# Returns and removes a key-value pair
export def --env "kv drop" [
key: string # Key of the kv-pair to drop
--universal (-u)
] {
let db_open = (db_setup --universal=$universal)
let value = (kv get --universal=$universal $key)
try {
do $db_open
# Hack to turn a SQLiteDatabase into a table
| query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $key }
}
if $universal and ($env.NU_KV_UNIVERSALS? | default false) {
hide-env $key
}
$value
}
def universal_db_path [] {
$env.NU_UNIVERSAL_KV_PATH?
| default (
$nu.data-dir | path join "std_kv_variables.sqlite3"
)
}
def db_setup [
--universal
] : nothing -> closure {
try {
match $universal {
true => {
# Ensure universal sqlite db and table exists
let uuid = (random uuid)
let dummy_record = {
session: ''
key: $uuid
value: ''
}
$dummy_record | into sqlite (universal_db_path) -t std_kv_store
open (universal_db_path) | query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $uuid }
}
false => {
# Create the stor table if it doesn't exist
stor create -t std_kv_store -c {session: str, key: str, value: str} | ignore
}
}
}
# Return the correct closure for opening on-disk vs. in-memory
match $universal {
true => {{|| open (universal_db_path)}}
false => {{|| stor open}}
}
}

234
nu/review.nu Normal file → Executable file
View File

@@ -25,15 +25,18 @@
# - Local Repo Review: just cr -f HEAD~1 --debug
# - Local PR Review: just cr -r hustcer/deepseek-review -n 32
use kv.nu *
use std-rfc/kv *
use diff.nu [get-diff]
use common.nu [
ECODE, NO_TOKEN_TIP, hr-line, is-installed, windows?, mac?,
compare-ver, compact-record, git-check, has-ref,
compare-ver, compact-record, git-check, has-ref, GITHUB_API_BASE
]
const RESPONSE_END = 'data: [DONE]'
const GITHUB_API_BASE = 'https://api.github.com'
const IGNORED_MESSAGES = {
'-alive': true, # The server is alive
'data: [DONE]': true, # The end of the response
': OPENROUTER PROCESSING': true, # OPENROUTER in PROCESSING message
}
# It takes longer to respond to requests made with unknown/rare user agents.
# When make http post pretend to be curl, it gets a response just as quickly as curl.
@@ -47,9 +50,6 @@ const DEFAULT_OPTIONS = {
SYS_PROMPT: 'You are a professional code review assistant responsible for analyzing code changes in GitHub Pull Requests. Identify potential issues such as code style violations, logical errors, security vulnerabilities, and provide improvement suggestions. Clearly list the problems and recommendations in a concise manner.',
}
# If the PR title or body contains any of these keywords, skip the review
const IGNORE_REVIEW_KEYWORDS = ['skip review' 'skip cr']
# Use DeepSeek AI to review code changes locally or in GitHub Actions
export def --env deepseek-review [
token?: string, # Your DeepSeek API token, fallback to CHAT_TOKEN env var
@@ -146,8 +146,9 @@ export def --env deepseek-review [
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 | default ($response | get -i message.content)
let message = $response | get -i choices.0.message
let reason = $message | coalesce-reasoning
let review = $message.content? | default ($response | get -i 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) ...'
@@ -225,19 +226,17 @@ def streaming-output [
}
| try { lines } catch { print $'(ansi r)Error Happened ...(ansi reset)'; exit $ECODE.SERVER_ERROR }
| each {|line|
if $line == $RESPONSE_END { return }
if ($line | is-empty) { return }
# DeepSeek Response vs Local Ollama Response
let $last = if $line =~ '^data: ' { $line | str substring 6.. | from json } else { $line | from json }
if $last == '-alive' { print $last; return }
if ($IGNORED_MESSAGES | get -i $line | default false) { return }
let $last = $line | parse-line
if $debug { $last | to json | kv set last-reply }
$last | get -i choices.0.delta | default ($last | get -i message) | if ($in | is-not-empty) {
let delta = $in
if ($delta.reasoning_content? | is-not-empty) { kv set reasoning ((kv get reasoning) + 1) }
if ($delta | coalesce-reasoning | is-not-empty) { kv set reasoning ((kv get reasoning) + 1) }
if (kv get reasoning) == 1 { print $'(char nl)Reasoning Details:'; hr-line }
if ($delta.content | is-not-empty) { kv set content ((kv get content) + 1) }
if (kv get content) == 1 { print $'(char nl)Review Details:'; hr-line }
print -n ($delta.reasoning_content? | default $delta.content)
print -n ($delta | coalesce-reasoning | default $delta.content)
}
}
@@ -247,197 +246,26 @@ def streaming-output [
}
}
# Get the diff content from GitHub PR or local git changes
export def get-diff [
--repo: string, # GitHub repository name
--pr-number: string, # GitHub PR number
--diff-to: string, # Diff to git ref
--diff-from: string, # Diff from git ref
--include: string, # Comma separated file patterns to include in the code review
--exclude: string, # Comma separated file patterns to exclude in the code review
--patch-cmd: string, # The `git show` or `git diff` command to get the diff content
] {
let local_repo = $env.PWD
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json]
let DIFF_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3.diff]
mut content = if ($pr_number | is-not-empty) {
if ($repo | is-empty) {
print $'(ansi r)Please provide the GitHub repository name by `--repo` option.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
# Parse the line from the streaming response
def parse-line [] {
let $line = $in
# DeepSeek Response vs Local Ollama Response
try {
if $line =~ '^data: ' {
$line | str substring 6.. | from json
} else {
$line | from json
}
# TODO: Ignore keywords checking when triggering by mentioning the bot
let description = http get -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)'
| select title body | values | str join "\n"
if ($IGNORE_REVIEW_KEYWORDS | any {|it| $description =~ $it }) {
print $'(ansi r)The PR title or body contains keywords to skip the review, bye...(ansi reset)'
exit $ECODE.SUCCESS
}
http get -H $DIFF_HEADER $'($GITHUB_API_BASE)/repos/($repo)/pulls/($pr_number)' | str trim
} else if ($diff_from | is-not-empty) {
if not (has-ref $diff_from) {
print $'(ansi r)The specified git ref ($diff_from) does not exist, please check it again.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
if ($diff_to | is-not-empty) and not (has-ref $diff_to) {
print $'(ansi r)The specified git ref ($diff_to) does not exist, please check it again.(ansi reset)'
exit $ECODE.INVALID_PARAMETER
}
git diff $diff_from ($diff_to | default HEAD)
} else if not (git-check $local_repo --check-repo=1) {
print $'Current directory ($local_repo) is (ansi r)NOT(ansi reset) a git repo, bye...(char nl)'
exit $ECODE.CONDITION_NOT_SATISFIED
} else if ($patch_cmd | is-not-empty) {
let valid = is-safe-git $patch_cmd
if not $valid { exit $ECODE.INVALID_PARAMETER }
nu -c $patch_cmd
} else { git diff }
if ($content | is-empty) {
print $'(ansi g)Nothing to review.(ansi reset)'; exit $ECODE.SUCCESS
} catch {
print -e $'(ansi r)Unrecognized content:(ansi reset) ($line)'
exit $ECODE.SERVER_ERROR
}
let awk_bin = (prepare-awk)
let outdated_awk = $'If you are using an (ansi r)outdated awk version(ansi reset), please upgrade to the latest version or use gawk latest instead.'
if ($include | is-not-empty) {
let patterns = $include | split row ','
$content = $content | try { ^$awk_bin (generate-include-regex $patterns) } catch { print $outdated_awk; exit $ECODE.OUTDATED }
}
if ($exclude | is-not-empty) {
let patterns = $exclude | split row ','
$content = $content | try { ^$awk_bin (generate-exclude-regex $patterns) } catch { print $outdated_awk; exit $ECODE.OUTDATED }
}
$content
}
# AWK family version check for both awk and gawk
# awk: awk version 20250116 -> 20250116
# gawk: GNU Awk 5.3.1, API 4.0, (GNU MPFR 4.2.1, GNU MP 6.3.0) -> 5.3.1
def get-awk-ver [awk_bin: string] {
^$awk_bin --version | lines | first | split row , | first | split row ' ' | last
}
# Prepare gawk for macOS
export def prepare-awk [] {
const MIN_GAWK_VERSION = '5.3.1'
const MIN_AWK_VERSION = '20250116'
let awk_installed = is-installed awk
let gawk_installed = is-installed gawk
if $awk_installed {
let awk_version = get-awk-ver awk
if (compare-ver $awk_version $MIN_AWK_VERSION) >= 0 {
print $'Current awk version: ($awk_version)'
return 'awk'
}
}
if $gawk_installed {
let gawk_version = get-awk-ver gawk
if (compare-ver $gawk_version $MIN_GAWK_VERSION) >= 0 {
print $'Current gawk version: ($gawk_version)'
return 'gawk'
} else if (windows?) and ($env.GITHUB_ACTIONS? == 'true') {
let awk_info = (install-gawk-for-actions)
print $'Current gawk version: ($awk_info.version)'
return $awk_info.awk_bin
}
}
if (mac?) and (is-installed brew) {
brew install gawk
print $'Current gawk version: (get-awk-ver gawk)'
return 'gawk'
}
if (not $awk_installed) and (not $gawk_installed) {
print $'(ansi r)Neither `awk` nor `gawk` is installed, please install the latest version of `gawk`.(ansi reset)'
exit $ECODE.MISSING_BINARY
}
print $'Current awk version: (get-awk-ver awk)'
'awk'
}
# Convert glob patterns to regex patterns
# Pass in *.nu directly as a regular expression does not work, because * in
# a regular expression needs to be attached to the previous pattern, the correct
# form should be .* So we should convert each glob pattern to a regex pattern:
# 1. Convert * to .*
# 2. Convert ? to . (optional, as needed)
# 3. Convert / to \/
def glob-to-regex [patterns: list<string>] {
# Handle empty patterns list
if ($patterns | length) == 0 { return '' }
# Define a mapping of characters to escape
let regex_escapes = {
# Escape special regex characters first
"\\.": "\\\\.",
"\\+": "\\\\+",
"\\^": "\\\\^",
"\\$": "\\\\$",
"\\(": "\\\\(",
"\\)": "\\\\)",
"\\[": "\\\\[",
"\\]": "\\\\]",
"\\{": "\\\\{",
"\\}": "\\\\}",
"\\|": "\\\\|",
# Then convert glob patterns to regex patterns
"*": ".*",
"?": ".",
"/": "\\/",
}
$patterns
| each { |pat|
$regex_escapes | columns | reduce -f $pat { |k, acc|
$acc | str replace -a $k ($regex_escapes | get $k)
}
}
| str join '|'
}
# Generate the awk include regex pattern string for the specified patterns
export def generate-include-regex [patterns: list<string>] {
let pattern = glob-to-regex $patterns
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}p"
}
# Generate the awk exclude regex pattern string for the specified patterns
export def generate-exclude-regex [patterns: list<string>] {
let pattern = glob-to-regex $patterns
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}!p"
}
# Check if the git command is safe to run in the shell
# Validate command examples:
# - git show
# - git diff
# - git show head~1
# - git diff 2393375 71f5a31
# - git diff 2393375 71f5a31 nu/*
# - git diff 2393375 71f5a31 :!nu/*
export def is-safe-git [cmd: string] {
let normalized_cmd = ($cmd | str trim | str downcase)
# Define allowed command patterns with named capture groups for better validation
let git_cmd_pattern = '^git\s+(show|diff)(?:\s+(?:[a-zA-Z0-9_\-\.~/]+)){0,3}(?:\s+(?::[!]?)?[a-zA-Z0-9_\-\.\*\/]+){0,2}$'
if ($normalized_cmd | find -r $git_cmd_pattern | is-empty) {
print $'(ansi r)Invalid git command format. (ansi g)Only simple `git show` or `git diff` commands are allowed.(ansi reset)'
return false
}
true
}
# Setup scoop and install gawk for GitHub Windows runners
# This command is essential for resolving the issue of simultaneously
# applying include and exclude patterns on GitHub's Windows runners.
def install-gawk-for-actions [] {
# Install scoop using PowerShell
pwsh -c r#'
Invoke-Expression (New-Object System.Net.WebClient).DownloadString("https://get.scoop.sh")
$env:Path = "$env:USERPROFILE\scoop\shims;" + $env:Path; scoop update; scoop install gawk
'# | complete | get stdout | print
let awk_bin = $'($nu.home-path)/scoop/shims/gawk.exe'
let version = get-awk-ver $awk_bin
{ awk_bin: $awk_bin, version: $version }
# Coalesce the reasoning content
def coalesce-reasoning [] {
let msg = $in
$msg.reasoning_content? | default $msg.reasoning?
}
alias main = deepseek-review

138
nu/util.nu Normal file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env nu
# Author: hustcer
# Created: 2025/04/02 20:02:15
#
use common.nu [ECODE, is-installed, compare-ver, windows?, mac?]
# AWK family version check for both awk and gawk
# awk: awk version 20250116 -> 20250116
# gawk: GNU Awk 5.3.1, API 4.0, (GNU MPFR 4.2.1, GNU MP 6.3.0) -> 5.3.1
def get-awk-ver [awk_bin: string] {
^$awk_bin --version | lines | first | split row , | first | split row ' ' | last
}
# Prepare gawk for macOS
export def prepare-awk [] {
const MIN_GAWK_VERSION = '5.3.1'
const MIN_AWK_VERSION = '20250116'
let awk_installed = is-installed awk
let gawk_installed = is-installed gawk
if $awk_installed {
let awk_version = get-awk-ver awk
if (compare-ver $awk_version $MIN_AWK_VERSION) >= 0 {
print $'Current awk version: ($awk_version)'
return 'awk'
}
}
if $gawk_installed {
let gawk_version = get-awk-ver gawk
if (compare-ver $gawk_version $MIN_GAWK_VERSION) >= 0 {
print $'Current gawk version: ($gawk_version)'
return 'gawk'
} else if (windows?) and ($env.GITHUB_ACTIONS? == 'true') {
let awk_info = (install-gawk-for-actions)
print $'Current gawk version: ($awk_info.version)'
return $awk_info.awk_bin
}
}
if (mac?) and (is-installed brew) {
brew install gawk
print $'Current gawk version: (get-awk-ver gawk)'
return 'gawk'
}
if (not $awk_installed) and (not $gawk_installed) {
print $'(ansi r)Neither `awk` nor `gawk` is installed, please install the latest version of `gawk`.(ansi reset)'
exit $ECODE.MISSING_BINARY
}
print $'Current awk version: (get-awk-ver awk)'
'awk'
}
# Convert glob patterns to regex patterns
# Pass in *.nu directly as a regular expression does not work, because * in
# a regular expression needs to be attached to the previous pattern, the correct
# form should be .* So we should convert each glob pattern to a regex pattern:
# 1. Convert * to .*
# 2. Convert ? to . (optional, as needed)
# 3. Convert / to \/
def glob-to-regex [patterns: list<string>] {
# Handle empty patterns list
if ($patterns | length) == 0 { return '' }
# Define a mapping of characters to escape
let regex_escapes = {
# Escape special regex characters first
"\\.": "\\\\.",
"\\+": "\\\\+",
"\\^": "\\\\^",
"\\$": "\\\\$",
"\\(": "\\\\(",
"\\)": "\\\\)",
"\\[": "\\\\[",
"\\]": "\\\\]",
"\\{": "\\\\{",
"\\}": "\\\\}",
"\\|": "\\\\|",
# Then convert glob patterns to regex patterns
"*": ".*",
"?": ".",
"/": "\\/",
}
$patterns
| each { |pat|
$regex_escapes | columns | reduce -f $pat { |k, acc|
$acc | str replace -a $k ($regex_escapes | get $k)
}
}
| str join '|'
}
# Generate the awk include regex pattern string for the specified patterns
export def generate-include-regex [patterns: list<string>] {
let pattern = glob-to-regex $patterns
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}p"
}
# Generate the awk exclude regex pattern string for the specified patterns
export def generate-exclude-regex [patterns: list<string>] {
let pattern = glob-to-regex $patterns
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}!p"
}
# Check if the git command is safe to run in the shell
# Validate command examples:
# - git show
# - git diff
# - git show head~1
# - git diff 2393375 71f5a31
# - git diff 2393375 71f5a31 nu/*
# - git diff 2393375 71f5a31 :!nu/*
export def is-safe-git [cmd: string] {
let normalized_cmd = ($cmd | str trim | str downcase)
# Define allowed command patterns with named capture groups for better validation
let git_cmd_pattern = '^git\s+(show|diff)(?:\s+(?:[a-zA-Z0-9_\-\.~/]+)){0,3}(?:\s+(?::[!]?)?[a-zA-Z0-9_\-\.\*\/]+){0,2}$'
if ($normalized_cmd | find -r $git_cmd_pattern | is-empty) {
print $'(ansi r)Invalid git command format. (ansi g)Only simple `git show` or `git diff` commands are allowed.(ansi reset)'
return false
}
true
}
# Setup scoop and install gawk for GitHub Windows runners
# This command is essential for resolving the issue of simultaneously
# applying include and exclude patterns on GitHub's Windows runners.
def install-gawk-for-actions [] {
# Install scoop using PowerShell
pwsh -c r#'
Invoke-Expression (New-Object System.Net.WebClient).DownloadString("https://get.scoop.sh")
$env:Path = "$env:USERPROFILE\scoop\shims;" + $env:Path; scoop update; scoop install gawk
'# | complete | get stdout | print
let awk_bin = $'($nu.home-path)/scoop/shims/gawk.exe'
let version = get-awk-ver $awk_bin
{ awk_bin: $awk_bin, version: $version }
}

View File

@@ -1,10 +1,7 @@
use std/assert
use ../nu/review.nu [
get-diff, is-safe-git, prepare-awk,
generate-include-regex, generate-exclude-regex,
]
use ../nu/diff.nu [get-diff]
use ../nu/util.nu [is-safe-git, prepare-awk, generate-include-regex, generate-exclude-regex]
# Get the unicode width of the input string
def get-uw [] { $in | str stats | get unicode-width }