Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
422bd581e0 | ||
|
|
a79d5106a0 | ||
|
|
01df74b01e | ||
|
|
b812ff62f8 | ||
|
|
7632652b7b | ||
|
|
10cbfc39a5 | ||
|
|
c7e8b449c9 | ||
|
|
6eda653db6 | ||
|
|
4d9674831b | ||
|
|
28c29e69df |
No files matched your search
@@ -49,6 +49,7 @@
|
|||||||
- 🛡️ **智能降噪过滤**:自动剥离 `go.sum`、`package-lock.json`、`*.pb.go`、`*.gen.go` 等锁文件与自动生成代码,省 Token、防死循环。
|
- 🛡️ **智能降噪过滤**:自动剥离 `go.sum`、`package-lock.json`、`*.pb.go`、`*.gen.go` 等锁文件与自动生成代码,省 Token、防死循环。
|
||||||
- 🎯 **严谨架构师准则**:直奔主题,聚焦并发竞态、死锁、SQL 注入、空指针、资源泄露,并直接提供修复参考代码。
|
- 🎯 **严谨架构师准则**:直奔主题,聚焦并发竞态、死锁、SQL 注入、空指针、资源泄露,并直接提供修复参考代码。
|
||||||
- 📊 **Token 消耗与费用实时估算**:实时捕获并统计输入/输出 Token 消耗,内置 2026 最新官方定价表精准核算每次审查成本(本地离线模型自动标注免费)。
|
- 📊 **Token 消耗与费用实时估算**:实时捕获并统计输入/输出 Token 消耗,内置 2026 最新官方定价表精准核算每次审查成本(本地离线模型自动标注免费)。
|
||||||
|
- ✨ **AI 智能 Commit Message**:一键根据代码变更生成规范的 Conventional Commits 提交说明,支持**中/英/日/韩**等多语言自适应。
|
||||||
- 📋 **团队规范适配(`.airules`)**:在项目根目录放置 `.airules`,AI 自动加载团队专属架构规范并最高优先级执行。
|
- 📋 **团队规范适配(`.airules`)**:在项目根目录放置 `.airules`,AI 自动加载团队专属架构规范并最高优先级执行。
|
||||||
- 🪝 **一键 Pre-commit 钩子**:一行命令挂载 Git 提交前自动拦截,在代码上库前把好第一道质量关。
|
- 🪝 **一键 Pre-commit 钩子**:一行命令挂载 Git 提交前自动拦截,在代码上库前把好第一道质量关。
|
||||||
|
|
||||||
@@ -145,6 +146,9 @@ git air config set --provider openai --key "YOUR_KEY" --model gpt-4o-mini
|
|||||||
|
|
||||||
# 13. 自定义 Token 计费费率 (可选:覆盖内置价格表,单位: 美元/1M Tokens)
|
# 13. 自定义 Token 计费费率 (可选:覆盖内置价格表,单位: 美元/1M Tokens)
|
||||||
git air config set --price-input 0.75 --price-output 3.75
|
git air config set --price-input 0.75 --price-output 3.75
|
||||||
|
|
||||||
|
# 14. 永久设置 Commit Message 默认语言 (可选: auto, zh, en, ja, ko)
|
||||||
|
git air config set --commit-lang en
|
||||||
```
|
```
|
||||||
|
|
||||||
查看当前配置状态(含自定义费率与脱敏 Key):
|
查看当前配置状态(含自定义费率与脱敏 Key):
|
||||||
@@ -175,7 +179,48 @@ git air --provider deepseek --model deepseek-chat
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. 一键安装 Git Pre-commit 拦截钩子
|
### 3. AI 智能生成规范 Commit Message (`git air msg`)
|
||||||
|
|
||||||
|
写代码一时爽,写 Commit 抓耳挠腮?`git-air` 会根据你的代码改动,自动生成符合国际 Conventional Commits 规范的精炼提交说明,并支持**中/英/日/韩多语言智能自适应**与**自动暂存**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 基础用法:分析暂存区代码并交互式确认提交
|
||||||
|
git air msg
|
||||||
|
|
||||||
|
# 2. 自动暂存所有改动并生成提交(无需手动 git add)
|
||||||
|
git air msg -a
|
||||||
|
|
||||||
|
# 3. 指定单次输出自然语言(支持 auto, zh, en, ja, ko)
|
||||||
|
git air msg -l en # 生成英文说明(适合 GitHub 开源项目)
|
||||||
|
git air msg -l ja # 生成日语说明
|
||||||
|
git air msg -l ko # 生成韩语说明
|
||||||
|
|
||||||
|
# 4. 极速一条龙:自动暂存 + 生成 + 免确认直接提交
|
||||||
|
git air msg -a -c
|
||||||
|
|
||||||
|
# 5. 永久修改全局默认提交语言(无需每次加 -l 参数)
|
||||||
|
git air config set --commit-lang en # 以后永久默认生成英文
|
||||||
|
git air config set --commit-lang ja # 以后永久默认生成日文
|
||||||
|
git air config set --commit-lang auto # 恢复自动跟随系统
|
||||||
|
```
|
||||||
|
|
||||||
|
**效果演示:**
|
||||||
|
```text
|
||||||
|
$ git air msg -l zh
|
||||||
|
[git-air] 正在生成 Conventional Commit Message... (Language: 简体中文 / Engine: gemini-3.7-flash)
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
feat(auth): 引入 Redis 缓存并修复 Mutex 死锁隐患
|
||||||
|
|
||||||
|
- 在用户认证模块新增 Redis Token 缓存机制
|
||||||
|
- 修复提前 return 时未释放互斥锁的并发隐患
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
? 是否直接以此信息执行 git commit?[Y/n]: y
|
||||||
|
[SUCCESS] 代码提交成功!🎉
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 一键安装 Git Pre-commit 拦截钩子
|
||||||
|
|
||||||
在你的 Git 项目根目录下执行:
|
在你的 Git 项目根目录下执行:
|
||||||
```bash
|
```bash
|
||||||
@@ -192,11 +237,25 @@ git air hook uninstall
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 5. 版本查看与一键自动升级 (`git air update`)
|
||||||
|
|
||||||
|
`git-air` 内置了**轻量级 24 小时静默更新检测**,当 GitHub 发布新版本时会在终端底部自动提醒;你也可以随时手动检查与一键升级:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看当前版本并在线检查更新
|
||||||
|
git air version
|
||||||
|
|
||||||
|
# 一键自动拉取并升级到最新版本
|
||||||
|
git air update
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⚙️ 配置文件说明
|
## ⚙️ 配置文件说明
|
||||||
|
|
||||||
`git-air` 支持多层配置覆盖,优先级从高到低为:
|
`git-air` 支持多层配置覆盖,优先级从高到低为:
|
||||||
1. **命令行 Flags**(如 `--key`, `--model`, `--provider`, `--strict`, `--no-block`)
|
1. **命令行 Flags**(如 `--key`, `--model`, `--provider`, `--lang`, `--strict`, `--no-block`)
|
||||||
2. **环境变量**(`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
2. **环境变量**(`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`, `GIT_AIR_COMMIT_LANG`)
|
||||||
3. **项目级配置**(当前 Git 项目根目录下的 `config.yaml` 或 `.git-air.yaml`)
|
3. **项目级配置**(当前 Git 项目根目录下的 `config.yaml` 或 `.git-air.yaml`)
|
||||||
4. **全局用户配置**(`~/.git-air/config.yaml`)
|
4. **全局用户配置**(`~/.git-air/config.yaml`)
|
||||||
|
|
||||||
@@ -217,9 +276,12 @@ model: "gemini-3.7-flash"
|
|||||||
# 自定义基础提示词 (可选,留空则使用默认资深架构师准则)
|
# 自定义基础提示词 (可选,留空则使用默认资深架构师准则)
|
||||||
# custom_prompt: ""
|
# custom_prompt: ""
|
||||||
|
|
||||||
# 自定义模型单价 (可选,单位: 美元/1M Tokens,用于精准核算自定义中转或企业私有接口费用)
|
# 自定义模型单价 (可选,单位: 美元/1M Tokens)
|
||||||
# price_input: 0.75
|
# price_input: 0.75
|
||||||
# price_output: 3.75
|
# price_output: 3.75
|
||||||
|
|
||||||
|
# AI Commit Message 生成语言 (可选: auto, zh, en, ja, ko)
|
||||||
|
# commit_lang: "auto"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+65
-3
@@ -48,6 +48,7 @@ Completely isolated from your chat history, it acts like a **grumpy but sharp se
|
|||||||
- 🛡️ **Smart Noise Reduction**: Automatically strips out `go.sum`, `package-lock.json`, `*.pb.go`, and lockfiles to save tokens and eliminate review loops.
|
- 🛡️ **Smart Noise Reduction**: Automatically strips out `go.sum`, `package-lock.json`, `*.pb.go`, and lockfiles to save tokens and eliminate review loops.
|
||||||
- 🎯 **Strict Senior Architect Persona**: Zero corporate fluff. Directly flags concurrency deadlocks, SQL injections, nil dereferences, and resource leaks with copy-paste code patches.
|
- 🎯 **Strict Senior Architect Persona**: Zero corporate fluff. Directly flags concurrency deadlocks, SQL injections, nil dereferences, and resource leaks with copy-paste code patches.
|
||||||
- 📊 **Real-time Token & Cost Estimation**: Accurately tracks prompt/completion token consumption with built-in official pricing rates (local offline models automatically marked free).
|
- 📊 **Real-time Token & Cost Estimation**: Accurately tracks prompt/completion token consumption with built-in official pricing rates (local offline models automatically marked free).
|
||||||
|
- ✨ **AI Commit Message Generator**: Automatically generates clean, standardized Conventional Commits messages with multi-language support (English, Japanese, Korean, Chinese).
|
||||||
- 📋 **Team Rules Support (`.airules`)**: Drop an `.airules` file in your repo root, and the AI will prioritize your team-specific guidelines.
|
- 📋 **Team Rules Support (`.airules`)**: Drop an `.airules` file in your repo root, and the AI will prioritize your team-specific guidelines.
|
||||||
- 🪝 **One-Click Pre-commit Hook**: Mounts as a local Git hook in one command to block dangerous bugs before code is committed.
|
- 🪝 **One-Click Pre-commit Hook**: Mounts as a local Git hook in one command to block dangerous bugs before code is committed.
|
||||||
|
|
||||||
@@ -144,6 +145,9 @@ git air config set --provider openai --key "YOUR_KEY" --model gpt-4o-mini
|
|||||||
|
|
||||||
# 13. Custom Token Pricing (Optional: override default rates, USD/1M tokens)
|
# 13. Custom Token Pricing (Optional: override default rates, USD/1M tokens)
|
||||||
git air config set --price-input 0.75 --price-output 3.75
|
git air config set --price-input 0.75 --price-output 3.75
|
||||||
|
|
||||||
|
# 14. Permanently Set Default Commit Message Language (auto, zh, en, ja, ko)
|
||||||
|
git air config set --commit-lang en
|
||||||
```
|
```
|
||||||
|
|
||||||
Check current configuration (including masked keys and custom rates):
|
Check current configuration (including masked keys and custom rates):
|
||||||
@@ -174,7 +178,48 @@ git air --provider deepseek --model deepseek-chat
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. One-Click Git Pre-commit Hook
|
### 3. AI-Powered Commit Message Generation (`git air msg`)
|
||||||
|
|
||||||
|
Struggling to write concise commit messages? `git-air` inspects your code changes and automatically crafts a clean Conventional Commits message with **auto-language adaptation** and **auto-staging**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Basic usage: analyze staged changes & interactively commit
|
||||||
|
git air msg
|
||||||
|
|
||||||
|
# 2. Auto-stage all modified files (no need for manual git add)
|
||||||
|
git air msg -a
|
||||||
|
|
||||||
|
# 3. Explicitly specify natural language for one-off run (auto, zh, en, ja, ko)
|
||||||
|
git air msg -l en # English (ideal for open-source repos)
|
||||||
|
git air msg -l ja # Japanese
|
||||||
|
git air msg -l ko # Korean
|
||||||
|
|
||||||
|
# 4. Instant one-liner: auto-stage + generate + direct commit
|
||||||
|
git air msg -a -c
|
||||||
|
|
||||||
|
# 5. Permanently set global default commit language (no need for -l flag)
|
||||||
|
git air config set --commit-lang en # Default to English forever
|
||||||
|
git air config set --commit-lang ja # Default to Japanese forever
|
||||||
|
git air config set --commit-lang auto # Reset to auto-detect system locale
|
||||||
|
```
|
||||||
|
|
||||||
|
**Terminal Preview:**
|
||||||
|
```text
|
||||||
|
$ git air msg -l en
|
||||||
|
[git-air] Generating Conventional Commit Message... (Language: English / Engine: gemini-3.7-flash)
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
feat(auth): add Redis token caching and fix mutex deadlock
|
||||||
|
|
||||||
|
- Introduce Redis caching layer for user authentication
|
||||||
|
- Fix concurrency bug where mutex was not unlocked on early return
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
? Execute git commit with this message? [Y/n]: y
|
||||||
|
[SUCCESS] Code committed successfully! 🎉
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. One-Click Git Pre-commit Hook
|
||||||
|
|
||||||
Install the hook inside any Git repository:
|
Install the hook inside any Git repository:
|
||||||
```bash
|
```bash
|
||||||
@@ -191,11 +236,25 @@ git air hook uninstall
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 5. Version Check & One-Click Upgrade (`git air update`)
|
||||||
|
|
||||||
|
`git-air` comes with **lightweight 24-hour background update detection** and alerts you in the terminal whenever a new release is available. You can also manually check or upgrade anytime:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current version and check for updates
|
||||||
|
git air version
|
||||||
|
|
||||||
|
# One-click upgrade to the latest release
|
||||||
|
git air update
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⚙️ Configuration Hierarchy
|
## ⚙️ Configuration Hierarchy
|
||||||
|
|
||||||
`git-air` searches for configurations in the following priority order:
|
`git-air` searches for configurations in the following priority order:
|
||||||
1. **CLI Flags** (`--key`, `--model`, `--provider`, `--prompt`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
1. **CLI Flags** (`--key`, `--model`, `--provider`, `--prompt`, `--lang`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||||
2. **Environment Variables** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
2. **Environment Variables** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`, `GIT_AIR_COMMIT_LANG`)
|
||||||
3. **Project Config** (`./config.yaml` or `./.git-air.yaml` in repo root)
|
3. **Project Config** (`./config.yaml` or `./.git-air.yaml` in repo root)
|
||||||
4. **Global User Config** (`~/.git-air/config.yaml`)
|
4. **Global User Config** (`~/.git-air/config.yaml`)
|
||||||
|
|
||||||
@@ -215,6 +274,9 @@ model: "gemini-3.7-flash"
|
|||||||
# Custom Token Pricing (Optional, in USD per 1M tokens)
|
# Custom Token Pricing (Optional, in USD per 1M tokens)
|
||||||
# price_input: 0.75
|
# price_input: 0.75
|
||||||
# price_output: 3.75
|
# price_output: 3.75
|
||||||
|
|
||||||
|
# AI Commit Message Language (Optional: auto, zh, en, ja, ko)
|
||||||
|
# commit_lang: "auto"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+128
-24
@@ -48,6 +48,7 @@ GitHubなどのCI/CD上で動作するボットも優秀ですが、**`git-air`
|
|||||||
- 🛡️ **スマートなノイズ除去**:`go.sum` や `package-lock.json`、自動生成された `*.pb.go` を自動除外。Token消費を抑え、思考ループを防ぎます。
|
- 🛡️ **スマートなノイズ除去**:`go.sum` や `package-lock.json`、自動生成された `*.pb.go` を自動除外。Token消費を抑え、思考ループを防ぎます。
|
||||||
- 🎯 **厳格なシニアエンジニア基準**:お世辞や無駄口は一切なし。並行処理の競合、デッドロック、SQLインジェクション、NULLポインタ、リソースリークを指摘し、具体的な修正コードを直接提示します。
|
- 🎯 **厳格なシニアエンジニア基準**:お世辞や無駄口は一切なし。並行処理の競合、デッドロック、SQLインジェクション、NULLポインタ、リソースリークを指摘し、具体的な修正コードを直接提示します。
|
||||||
- 📊 **Token消費量と費用のリアルタイム見積もり**:入出力Token数を正確に集計し、各社最新の公式料金表に基づいて実行コストを即座に計算(ローカル完全オフラインモデルは無料表示)。
|
- 📊 **Token消費量と費用のリアルタイム見積もり**:入出力Token数を正確に集計し、各社最新の公式料金表に基づいて実行コストを即座に計算(ローカル完全オフラインモデルは無料表示)。
|
||||||
|
- ✨ **AI コミットメッセージ自動生成**:変更差分から Conventional Commits 準拠の高品質なメッセージを生成(**日本語・英語・韓国語・中国語**に自動適応)。
|
||||||
- 📋 **チーム規約の読み込み(`.airules`)**:リポジトリ直下に `.airules` を配置するだけで、チーム固有のアーキテクチャ規約を最優先で適用。
|
- 📋 **チーム規約の読み込み(`.airules`)**:リポジトリ直下に `.airules` を配置するだけで、チーム固有のアーキテクチャ規約を最優先で適用。
|
||||||
- 🪝 **ワンクリックで Pre-commit フック化**:コマンド1発でGitフックとして登録。危険なコードのコミットを未然にブロックします。
|
- 🪝 **ワンクリックで Pre-commit フック化**:コマンド1発でGitフックとして登録。危険なコードのコミットを未然にブロックします。
|
||||||
|
|
||||||
@@ -59,37 +60,79 @@ GitHubなどのCI/CD上で動作するボットも優秀ですが、**`git-air`
|
|||||||
$ git air
|
$ git air
|
||||||
|
|
||||||
[git-air] コードレビュー中... (Engine: gemini / gemini-3.7-flash)
|
[git-air] コードレビュー中... (Engine: gemini / gemini-3.7-flash)
|
||||||
─────────────────────────────────────────────────────────────────
|
────────────────────────────────# 13. カスタム Token 料金設定 (任意:組み込み料金表を上書き、単位: 米ドル/1M Tokens)
|
||||||
#### 変更概要
|
git air config set --price-input 0.75 --price-output 3.75
|
||||||
ユーザー認証ハンドラーに Redis キャッシュ層を追加し、Context のタイムアウト伝播を修正。
|
|
||||||
|
|
||||||
#### 詳細レビュー結果
|
# 14. コミットメッセージのデフォルト言語を永続設定 (任意: auto, zh, en, ja, ko)
|
||||||
- [BLOCKER] internal/service/user.go:45 - 重大なセキュリティ脆弱性: ループ内で生SQL文字列を直接結合しています。SQLインジェクションのリスクがあります。
|
git air config set --commit-lang ja
|
||||||
// 推奨修正コード:
|
```
|
||||||
db.Where("username = ?", inputName).First(&user)
|
|
||||||
|
|
||||||
- [WARNING] internal/service/user.go:82 - 潜在的リスク: Redis 接続エラーを握りつぶしています。キャッシュ障害時にDBへ負荷が集中します。
|
現在の設定を確認(カスタム料金やマスク化Keyを含む):
|
||||||
|
```bash
|
||||||
- [WARNING] internal/service/user.go:103 - 並行処理の欠陥: 早期リターン時に Mutex の Unlock を忘れています(Mutex Leak)。デッドロックの危険性があります。
|
git air config get
|
||||||
|
|
||||||
#### 結論
|
|
||||||
- 判定: [REJECT]
|
|
||||||
- スコア: 60 / 100
|
|
||||||
|
|
||||||
📊 Token: 入力 1,243 / 出力 412 | ≈ $0.0025
|
|
||||||
─────────────────────────────────────────────────────────────────
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 インストール
|
### 2. よく使うレビューコマンド
|
||||||
|
|
||||||
### 方法 1: Go Install(推奨)
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/unclesam-ly/git-air@latest
|
# 現在ステージングされた(git add済み)変更をレビュー(デフォルト)
|
||||||
|
git air
|
||||||
|
|
||||||
|
# 直前のコミット内容をレビュー
|
||||||
|
git air HEAD~1
|
||||||
|
|
||||||
|
# ブランチ間の差分をレビュー
|
||||||
|
git air main..feature-agent
|
||||||
|
|
||||||
|
# 特定のファイルのみをレビュー
|
||||||
|
git air internal/service/chat.go
|
||||||
|
|
||||||
|
# 一時的にモデルやProviderを指定して実行
|
||||||
|
git air --provider deepseek --model deepseek-chat
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方法 2: ソースコードからビルド
|
---
|
||||||
|
|
||||||
|
### 3. AI コミットメッセージ自動生成 (`git air msg`)
|
||||||
|
|
||||||
|
コミットメッセージの作成に悩む必要はもうありません。`git-air` がコードの変更差分を分析し、Conventional Commits 準拠のメッセージを**多言語自動適応**と**自動ステージング**で生成します:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 基本的な使い方:差分を分析して対話型コミット
|
||||||
|
git air msg
|
||||||
|
|
||||||
|
# 2. 変更ファイルを自動ステージング(git add 不要)
|
||||||
|
git air msg -a
|
||||||
|
|
||||||
|
# 3. 単発で出力言語を指定(auto, zh, en, ja, ko)
|
||||||
|
git air msg -l en # 英語(オープンソース・国際プロジェクト向け)
|
||||||
|
git air msg -l ja # 日本語
|
||||||
|
git air msg -l ko # 韓国語
|
||||||
|
|
||||||
|
# 4. 最速ワンライナー:自動ステージング + 生成 + 即座にコミット
|
||||||
|
git air msg -a -c
|
||||||
|
|
||||||
|
# 5. グローバルデフォルト言語を永続設定(毎回 -l を付ける必要がなくなります)
|
||||||
|
git air config set --commit-lang ja # 今後常に日本語で生成
|
||||||
|
git air config set --commit-lang en # 今後常に英語で生成
|
||||||
|
git air config set --commit-lang auto # システム言語への自動追従に戻す
|
||||||
|
```
|
||||||
|
|
||||||
|
**実行イメージ:**
|
||||||
|
```text
|
||||||
|
$ git air msg -l ja
|
||||||
|
[git-air] Conventional Commit Message を生成中... (Language: 日本語 / Engine: gemini-3.7-flash)
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
feat(auth): Redisキャッシュを追加し、Mutexのデッドロックを修正
|
||||||
|
|
||||||
|
- ユーザー認証ハンドラーに Redis トークンキャッシュ層を導入
|
||||||
|
- 早期リターン時に Mutex の Unlock が漏れる並行処理のバグを修正
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
? このメッセージで直接 git commit を実行しますか?[Y/n]: y
|
||||||
|
[SUCCESS] コードのコミットに成功しました!🎉
|
||||||
|
```スコードからビルド
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/unclesam-ly/git-air.git
|
git clone https://github.com/unclesam-ly/git-air.git
|
||||||
cd git-air
|
cd git-air
|
||||||
@@ -144,6 +187,9 @@ git air config set --provider openai --key "YOUR_KEY" --model gpt-4o-mini
|
|||||||
|
|
||||||
# 13. カスタム Token 料金設定 (任意:組み込み料金表を上書き、単位: 米ドル/1M Tokens)
|
# 13. カスタム Token 料金設定 (任意:組み込み料金表を上書き、単位: 米ドル/1M Tokens)
|
||||||
git air config set --price-input 0.75 --price-output 3.75
|
git air config set --price-input 0.75 --price-output 3.75
|
||||||
|
|
||||||
|
# 14. コミットメッセージのデフォルト言語を永続設定 (任意: auto, zh, en, ja, ko)
|
||||||
|
git air config set --commit-lang ja
|
||||||
```
|
```
|
||||||
|
|
||||||
現在の設定を確認(カスタム料金やマスク化Keyを含む):
|
現在の設定を確認(カスタム料金やマスク化Keyを含む):
|
||||||
@@ -174,7 +220,48 @@ git air --provider deepseek --model deepseek-chat
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. ワンタッチで Pre-commit フックを登録
|
### 3. AI コミットメッセージ自動生成 (`git air msg`)
|
||||||
|
|
||||||
|
コミットメッセージの作成に悩む必要はもうありません。`git-air` がコードの変更差分を分析し、Conventional Commits 準拠のメッセージを**多言語自動適応**と**自動ステージング**で生成します:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 基本的な使い方:差分を分析して対話型コミット
|
||||||
|
git air msg
|
||||||
|
|
||||||
|
# 2. 変更ファイルを自動ステージング(git add 不要)
|
||||||
|
git air msg -a
|
||||||
|
|
||||||
|
# 3. 単発で出力言語を指定(auto, zh, en, ja, ko)
|
||||||
|
git air msg -l en # 英語(オープンソース・国際プロジェクト向け)
|
||||||
|
git air msg -l ja # 日本語
|
||||||
|
git air msg -l ko # 韓国語
|
||||||
|
|
||||||
|
# 4. 最速ワンライナー:自動ステージング + 生成 + 即座にコミット
|
||||||
|
git air msg -a -c
|
||||||
|
|
||||||
|
# 5. グローバルデフォルト言語を永続設定(毎回 -l を付ける必要がなくなります)
|
||||||
|
git air config set --commit-lang ja # 今後常に日本語で生成
|
||||||
|
git air config set --commit-lang en # 今後常に英語で生成
|
||||||
|
git air config set --commit-lang auto # システム言語への自動追従に戻す
|
||||||
|
```
|
||||||
|
|
||||||
|
**実行イメージ:**
|
||||||
|
```text
|
||||||
|
$ git air msg -l ja
|
||||||
|
[git-air] Conventional Commit Message を生成中... (Language: 日本語 / Engine: gemini-3.7-flash)
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
feat(auth): Redisキャッシュを追加し、Mutexのデッドロックを修正
|
||||||
|
|
||||||
|
- ユーザー認証ハンドラーに Redis トークンキャッシュ層を導入
|
||||||
|
- 早期リターン時に Mutex の Unlock が漏れる並行処理のバグを修正
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
? このメッセージで直接 git commit を実行しますか?[Y/n]: y
|
||||||
|
[SUCCESS] コードのコミットに成功しました!🎉
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. ワンタッチで Pre-commit フックを登録
|
||||||
|
|
||||||
Git リポジトリ直下で以下を実行:
|
Git リポジトリ直下で以下を実行:
|
||||||
```bash
|
```bash
|
||||||
@@ -191,10 +278,24 @@ git air hook uninstall
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 5. バージョン確認とワンクリック自動更新 (`git air update`)
|
||||||
|
|
||||||
|
`git-air` には**軽量な24時間サイレント更新検出機能**が組み込まれており、新しいリリースが公開されるとターミナル下部で自動通知されます。手動での確認やアップデートも可能です:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 現在のバージョンを確認し、更新をチェック
|
||||||
|
git air version
|
||||||
|
|
||||||
|
# 最新バージョンへ一括アップデート
|
||||||
|
git air update
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⚙️ 設定の優先順位
|
## ⚙️ 設定の優先順位
|
||||||
|
|
||||||
1. **コマンドライン引数** (`--key`, `--model`, `--provider`, `--prompt`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
1. **コマンドライン引数** (`--key`, `--model`, `--provider`, `--prompt`, `--lang`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||||
2. **環境変数** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
2. **環境変数** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`, `GIT_AIR_COMMIT_LANG`)
|
||||||
3. **プロジェクト個別設定** (リポジトリ直下の `./config.yaml` または `./.git-air.yaml`)
|
3. **プロジェクト個別設定** (リポジトリ直下の `./config.yaml` または `./.git-air.yaml`)
|
||||||
4. **グローバル設定** (`~/.git-air/config.yaml`)
|
4. **グローバル設定** (`~/.git-air/config.yaml`)
|
||||||
|
|
||||||
@@ -214,6 +315,9 @@ model: "gemini-3.7-flash"
|
|||||||
# カスタム Token 料金設定 (任意、単位: 米ドル / 1M Tokens)
|
# カスタム Token 料金設定 (任意、単位: 米ドル / 1M Tokens)
|
||||||
# price_input: 0.75
|
# price_input: 0.75
|
||||||
# price_output: 3.75
|
# price_output: 3.75
|
||||||
|
|
||||||
|
# AI コミットメッセージ言語設定 (任意: auto, zh, en, ja, ko)
|
||||||
|
# commit_lang: "auto"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type AppConfig struct {
|
|||||||
CustomPrompt string `yaml:"custom_prompt"` // 用户自定义的 Base System Prompt
|
CustomPrompt string `yaml:"custom_prompt"` // 用户自定义的 Base System Prompt
|
||||||
PriceInput float64 `yaml:"price_input,omitempty"` // 自定义输入单价 (每 1M tokens 美元,可选)
|
PriceInput float64 `yaml:"price_input,omitempty"` // 自定义输入单价 (每 1M tokens 美元,可选)
|
||||||
PriceOutput float64 `yaml:"price_output,omitempty"` // 自定义输出单价 (每 1M tokens 美元,可选)
|
PriceOutput float64 `yaml:"price_output,omitempty"` // 自定义输出单价 (每 1M tokens 美元,可选)
|
||||||
|
CommitLang string `yaml:"commit_lang,omitempty"` // Commit Message 生成语言 (auto, zh, en, ja, ko 等)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigPath() (string, error) {
|
func getConfigPath() (string, error) {
|
||||||
@@ -77,6 +78,9 @@ func LoadConfig() (*AppConfig, error) {
|
|||||||
if envBaseURL := os.Getenv("GIT_AIR_BASE_URL"); envBaseURL != "" {
|
if envBaseURL := os.Getenv("GIT_AIR_BASE_URL"); envBaseURL != "" {
|
||||||
cfg.BaseURL = envBaseURL
|
cfg.BaseURL = envBaseURL
|
||||||
}
|
}
|
||||||
|
if envLang := os.Getenv("GIT_AIR_COMMIT_LANG"); envLang != "" {
|
||||||
|
cfg.CommitLang = envLang
|
||||||
|
}
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
@@ -104,6 +108,7 @@ var configSetCmd = &cobra.Command{
|
|||||||
Short: "设置全局配置项",
|
Short: "设置全局配置项",
|
||||||
Example: ` git air config set --provider gemini --key YOUR_KEY
|
Example: ` git air config set --provider gemini --key YOUR_KEY
|
||||||
git air config set --provider deepseek --key YOUR_KEY --model deepseek-chat
|
git air config set --provider deepseek --key YOUR_KEY --model deepseek-chat
|
||||||
|
git air config set --commit-lang ja
|
||||||
git air config set --provider ollama --model qwen2.5-coder`,
|
git air config set --provider ollama --model qwen2.5-coder`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
cfg, err := LoadConfig()
|
cfg, err := LoadConfig()
|
||||||
@@ -134,6 +139,9 @@ var configSetCmd = &cobra.Command{
|
|||||||
if po, _ := cmd.Flags().GetFloat64("price-output"); po > 0 {
|
if po, _ := cmd.Flags().GetFloat64("price-output"); po > 0 {
|
||||||
cfg.PriceOutput = po
|
cfg.PriceOutput = po
|
||||||
}
|
}
|
||||||
|
if cl, _ := cmd.Flags().GetString("commit-lang"); cl != "" {
|
||||||
|
cfg.CommitLang = cl
|
||||||
|
}
|
||||||
if err := SaveConfig(cfg); err != nil {
|
if err := SaveConfig(cfg); err != nil {
|
||||||
ui.PrintError("保存配置失败: %v", err)
|
ui.PrintError("保存配置失败: %v", err)
|
||||||
return
|
return
|
||||||
@@ -168,6 +176,11 @@ var configGetCmd = &cobra.Command{
|
|||||||
hasPrompt = "是 (已自定义)"
|
hasPrompt = "是 (已自定义)"
|
||||||
}
|
}
|
||||||
fmt.Printf("CustomPrompt: %s\n", hasPrompt)
|
fmt.Printf("CustomPrompt: %s\n", hasPrompt)
|
||||||
|
commitLang := "auto (自动识别系统语言)"
|
||||||
|
if cfg.CommitLang != "" {
|
||||||
|
commitLang = cfg.CommitLang
|
||||||
|
}
|
||||||
|
fmt.Printf("CommitLang: %s\n", commitLang)
|
||||||
if cfg.PriceInput > 0 || cfg.PriceOutput > 0 {
|
if cfg.PriceInput > 0 || cfg.PriceOutput > 0 {
|
||||||
fmt.Printf("CustomPrice: 输入 $%.4f/1M, 输出 $%.4f/1M (已自定义覆盖)\n", cfg.PriceInput, cfg.PriceOutput)
|
fmt.Printf("CustomPrice: 输入 $%.4f/1M, 输出 $%.4f/1M (已自定义覆盖)\n", cfg.PriceInput, cfg.PriceOutput)
|
||||||
} else {
|
} else {
|
||||||
@@ -184,6 +197,7 @@ func init() {
|
|||||||
configSetCmd.Flags().String("prompt", "", "自定义 Base System Prompt")
|
configSetCmd.Flags().String("prompt", "", "自定义 Base System Prompt")
|
||||||
configSetCmd.Flags().Float64("price-input", 0, "自定义每 1M 输入 Token 美元价格")
|
configSetCmd.Flags().Float64("price-input", 0, "自定义每 1M 输入 Token 美元价格")
|
||||||
configSetCmd.Flags().Float64("price-output", 0, "自定义每 1M 输出 Token 美元价格")
|
configSetCmd.Flags().Float64("price-output", 0, "自定义每 1M 输出 Token 美元价格")
|
||||||
|
configSetCmd.Flags().String("commit-lang", "", "Commit Message 语言 (auto, zh, en, ja, ko 等)")
|
||||||
|
|
||||||
configCmd.AddCommand(configSetCmd)
|
configCmd.AddCommand(configSetCmd)
|
||||||
configCmd.AddCommand(configGetCmd)
|
configCmd.AddCommand(configGetCmd)
|
||||||
|
|||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/git"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/llm"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/reviewer"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
var msgCmd = &cobra.Command{
|
||||||
|
Use: "msg [git diff 参数,如 HEAD~1, main..feature, 文件名等]",
|
||||||
|
Aliases: []string{"commit-msg", "commit"},
|
||||||
|
Short: "根据代码变更智能生成规范的 Conventional Commits 提交信息",
|
||||||
|
Example: ` git air msg # 根据当前暂存区代码自动生成
|
||||||
|
git air msg -l ja # 指定生成日语提交说明
|
||||||
|
git air msg -l en # 指定生成英文提交说明
|
||||||
|
git air msg -c # 生成后直接执行 git commit`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
|
||||||
|
// 1. 检查并获取 Diff (默认优先获取暂存区)
|
||||||
|
diff, err := git.GetDiff(ctx, args...)
|
||||||
|
if err != nil {
|
||||||
|
ui.PrintError("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 加载配置
|
||||||
|
cfg, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
ui.PrintError("加载配置失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagKey, _ := cmd.Flags().GetString("key"); flagKey != "" {
|
||||||
|
cfg.APIKey = flagKey
|
||||||
|
}
|
||||||
|
if flagModel, _ := cmd.Flags().GetString("model"); flagModel != "" {
|
||||||
|
cfg.Model = flagModel
|
||||||
|
}
|
||||||
|
if flagProvider, _ := cmd.Flags().GetString("provider"); flagProvider != "" {
|
||||||
|
cfg.Provider = flagProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 初始化大模型客户端
|
||||||
|
llmClient, err := llm.NewClient(llm.Config{
|
||||||
|
Provider: cfg.Provider,
|
||||||
|
APIKey: cfg.APIKey,
|
||||||
|
BaseURL: cfg.BaseURL,
|
||||||
|
Model: cfg.Model,
|
||||||
|
PriceInput: cfg.PriceInput,
|
||||||
|
PriceOutput: cfg.PriceOutput,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ui.PrintError("初始化模型失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 确定目标语言 (命令行 Flag > 配置文件 > 自动探测系统语言)
|
||||||
|
lang, _ := cmd.Flags().GetString("lang")
|
||||||
|
if lang == "" {
|
||||||
|
lang = cfg.CommitLang
|
||||||
|
}
|
||||||
|
|
||||||
|
repoRoot, _ := git.GetRepoRoot(ctx)
|
||||||
|
rev := reviewer.NewReviewer(llmClient, repoRoot, "")
|
||||||
|
|
||||||
|
targetLang := reviewer.DetectLanguage(lang)
|
||||||
|
langDesc := "English"
|
||||||
|
switch targetLang {
|
||||||
|
case "zh":
|
||||||
|
langDesc = "简体中文"
|
||||||
|
case "ja":
|
||||||
|
langDesc = "日本語"
|
||||||
|
case "ko":
|
||||||
|
langDesc = "한국어"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\033[1;36m[git-air]\033[0m 正在生成 Conventional Commit Message... \033[0;90m(Language: %s / Engine: %s)\033[0m\n", langDesc, cfg.Model)
|
||||||
|
fmt.Println("\033[0;90m" + strings.Repeat("─", 65) + "\033[0m")
|
||||||
|
|
||||||
|
commitMsg, err := rev.GenerateCommitMsg(ctx, diff, lang)
|
||||||
|
if err != nil {
|
||||||
|
ui.PrintError("生成 Commit Message 失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 打印生成的提交信息
|
||||||
|
fmt.Printf("\033[1;32m%s\033[0m\n", commitMsg)
|
||||||
|
fmt.Println("\033[0;90m" + strings.Repeat("─", 65) + "\033[0m")
|
||||||
|
|
||||||
|
// 6. 是否自动暂存与提交
|
||||||
|
flagAll, _ := cmd.Flags().GetBool("all")
|
||||||
|
flagCommit, _ := cmd.Flags().GetBool("commit")
|
||||||
|
|
||||||
|
if flagCommit {
|
||||||
|
if flagAll {
|
||||||
|
if err := git.StageAll(ctx); err != nil {
|
||||||
|
ui.PrintError("自动暂存失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executeGitCommit(ctx, commitMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查暂存区状态
|
||||||
|
hasStaged, _ := git.HasStagedChanges(ctx)
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
if !hasStaged && !flagAll {
|
||||||
|
fmt.Print("? 检测到改动尚未暂存 (unstaged),是否自动暂存并提交 (git add -A)?[Y/n]: ")
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(strings.ToLower(input))
|
||||||
|
|
||||||
|
if input == "" || input == "y" || input == "yes" {
|
||||||
|
if err := git.StageAll(ctx); err != nil {
|
||||||
|
ui.PrintError("自动暂存失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
executeGitCommit(ctx, commitMsg)
|
||||||
|
} else {
|
||||||
|
fmt.Println("\033[0;90m已跳过提交。请执行 'git add <文件>' 暂存修改后再提交。\033[0m")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交互式询问是否直接提交
|
||||||
|
fmt.Print("? 是否直接以此信息执行 git commit?[Y/n]: ")
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(strings.ToLower(input))
|
||||||
|
|
||||||
|
if input == "" || input == "y" || input == "yes" {
|
||||||
|
executeGitCommit(ctx, commitMsg)
|
||||||
|
} else {
|
||||||
|
fmt.Println("\033[0;90m已跳过提交。你可以复制上方内容手动提交。\033[0m")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeGitCommit(ctx context.Context, msg string) {
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "commit", "-m", msg)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
ui.PrintError("执行 git commit 失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.PrintSuccess("代码提交成功!🎉")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
msgCmd.Flags().StringP("lang", "l", "", "生成语言 (auto, zh, en, ja, ko)")
|
||||||
|
msgCmd.Flags().BoolP("all", "a", false, "自动暂存所有已修改的文件 (git add -A)")
|
||||||
|
msgCmd.Flags().BoolP("commit", "c", false, "生成后无需确认直接执行 git commit")
|
||||||
|
msgCmd.Flags().StringP("key", "k", "", "临时指定 API Key")
|
||||||
|
msgCmd.Flags().StringP("model", "m", "", "临时指定模型名称")
|
||||||
|
msgCmd.Flags().StringP("provider", "p", "", "临时指定 Provider")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(msgCmd)
|
||||||
|
}
|
||||||
+14
-1
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/unclesam-ly/git-air/internal/llm"
|
"github.com/unclesam-ly/git-air/internal/llm"
|
||||||
"github.com/unclesam-ly/git-air/internal/reviewer"
|
"github.com/unclesam-ly/git-air/internal/reviewer"
|
||||||
"github.com/unclesam-ly/git-air/internal/ui"
|
"github.com/unclesam-ly/git-air/internal/ui"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/updater"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -21,6 +22,9 @@ var rootCmd = &cobra.Command{
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
|
|
||||||
|
// 后台非阻塞异步检查新版本
|
||||||
|
updateCh := updater.CheckAsync()
|
||||||
|
|
||||||
// 1. 检查并获取 Diff
|
// 1. 检查并获取 Diff
|
||||||
if flagCached, _ := cmd.Flags().GetBool("cached"); flagCached {
|
if flagCached, _ := cmd.Flags().GetBool("cached"); flagCached {
|
||||||
args = append([]string{"--cached"}, args...)
|
args = append([]string{"--cached"}, args...)
|
||||||
@@ -96,7 +100,16 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
ui.PrintFooter()
|
ui.PrintFooter()
|
||||||
|
|
||||||
// 8. 门禁阻断检测 (Pre-commit 拦截逻辑)
|
// 8. 检查新版本提醒(非阻塞)
|
||||||
|
select {
|
||||||
|
case info := <-updateCh:
|
||||||
|
if info != nil {
|
||||||
|
ui.PrintUpdateBanner(info.CurrentVersion, info.LatestVersion, info.ReleaseURL)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 门禁阻断检测 (Pre-commit 拦截逻辑)
|
||||||
outText := reviewOutput.String()
|
outText := reviewOutput.String()
|
||||||
hasBlocker := strings.Contains(outText, "[BLOCKER]") || strings.Contains(outText, "[CRITICAL]")
|
hasBlocker := strings.Contains(outText, "[BLOCKER]") || strings.Contains(outText, "[CRITICAL]")
|
||||||
hasReject := strings.Contains(outText, "[REJECT]")
|
hasReject := strings.Contains(outText, "[REJECT]")
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/ui"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateCmd = &cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Aliases: []string{"upgrade"},
|
||||||
|
Short: "一键升级 git-air 到最新版本",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Printf("🔍 正在检查并拉取最新版本 (当前: %s)...\n", updater.CurrentVersion)
|
||||||
|
|
||||||
|
updateCmd := exec.Command("go", "install", "github.com/unclesam-ly/git-air@latest")
|
||||||
|
updateCmd.Stdout = os.Stdout
|
||||||
|
updateCmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := updateCmd.Run(); err != nil {
|
||||||
|
ui.PrintError("升级失败: %v (提示: 请确保已安装 Go 并配置环境变量)", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.PrintSuccess("git-air 已成功升级至最新版本!🎉")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/ui"
|
||||||
|
"github.com/unclesam-ly/git-air/internal/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
var versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "查看当前 git-air 版本信息及更新检测",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Printf("git-air %s (%s/%s, Go %s)\n",
|
||||||
|
updater.CurrentVersion,
|
||||||
|
runtime.GOOS,
|
||||||
|
runtime.GOARCH,
|
||||||
|
runtime.Version(),
|
||||||
|
)
|
||||||
|
|
||||||
|
checkFlag, _ := cmd.Flags().GetBool("check")
|
||||||
|
if checkFlag {
|
||||||
|
info := updater.CheckUpdate(true)
|
||||||
|
if info != nil {
|
||||||
|
ui.PrintUpdateBanner(info.CurrentVersion, info.LatestVersion, info.ReleaseURL)
|
||||||
|
} else {
|
||||||
|
ui.PrintSuccess("当前已是最新版本!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
versionCmd.Flags().BoolP("check", "c", true, "向 GitHub 检查是否有可用新版本")
|
||||||
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
}
|
||||||
@@ -38,3 +38,6 @@ custom_prompt: ""
|
|||||||
# 若你的中转平台价格有变或使用私有计费,可在此自定义覆盖:
|
# 若你的中转平台价格有变或使用私有计费,可在此自定义覆盖:
|
||||||
# price_input: 0.75 # 每 1M 输入 Token 美元单价
|
# price_input: 0.75 # 每 1M 输入 Token 美元单价
|
||||||
# price_output: 3.75 # 每 1M 输出 Token 美元单价
|
# price_output: 3.75 # 每 1M 输出 Token 美元单价
|
||||||
|
|
||||||
|
# AI Commit Message 生成语言 (可选,默认 "auto" 自动跟随系统语言,可选: zh, en, ja, ko)
|
||||||
|
commit_lang: "auto"
|
||||||
@@ -32,6 +32,21 @@ func GetRepoRoot(ctx context.Context) (string, error) {
|
|||||||
return strings.TrimSpace(string(out)), nil
|
return strings.TrimSpace(string(out)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasStagedChanges 检查当前 Git 暂存区是否有待提交的文件改动
|
||||||
|
func HasStagedChanges(ctx context.Context) (bool, error) {
|
||||||
|
stagedDiff, err := execGitDiff(ctx, "--cached")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(stagedDiff) != "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StageAll 执行 git add -A 暂存当前所有改动
|
||||||
|
func StageAll(ctx context.Context) error {
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "add", "-A")
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
const maxDiffSize = 300 * 1024 // 限制最大 300KB Diff,避免打爆上下文与 OOM
|
const maxDiffSize = 300 * 1024 // 限制最大 300KB Diff,避免打爆上下文与 OOM
|
||||||
|
|
||||||
// GetDiff 提取并过滤 Git 代码差异
|
// GetDiff 提取并过滤 Git 代码差异
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package reviewer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetectLanguage 智能解析目标自然语言
|
||||||
|
// 优先级:传入指定语言 -> 系统 LANG/LC_ALL 环境变量 -> 默认 "en"
|
||||||
|
func DetectLanguage(specifiedLang string) string {
|
||||||
|
lang := strings.ToLower(strings.TrimSpace(specifiedLang))
|
||||||
|
if lang != "" && lang != "auto" {
|
||||||
|
return NormalizeLang(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从系统环境变量中探测
|
||||||
|
envLang := os.Getenv("LC_ALL")
|
||||||
|
if envLang == "" {
|
||||||
|
envLang = os.Getenv("LANG")
|
||||||
|
}
|
||||||
|
if envLang == "" {
|
||||||
|
envLang = os.Getenv("LC_MESSAGES")
|
||||||
|
}
|
||||||
|
|
||||||
|
envLang = strings.ToLower(envLang)
|
||||||
|
if strings.HasPrefix(envLang, "zh") {
|
||||||
|
return "zh"
|
||||||
|
} else if strings.HasPrefix(envLang, "ja") {
|
||||||
|
return "ja"
|
||||||
|
} else if strings.HasPrefix(envLang, "ko") {
|
||||||
|
return "ko"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeLang 规范化语言缩写
|
||||||
|
func NormalizeLang(lang string) string {
|
||||||
|
switch strings.ToLower(lang) {
|
||||||
|
case "zh", "cn", "chinese", "zh-cn", "zh-tw", "中文":
|
||||||
|
return "zh"
|
||||||
|
case "ja", "jp", "japanese", "ja-jp", "日本語", "日文":
|
||||||
|
return "ja"
|
||||||
|
case "ko", "kr", "korean", "ko-kr", "한국어", "韩文":
|
||||||
|
return "ko"
|
||||||
|
default:
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildCommitMsgPrompt 构建用于生成 Conventional Commits 的专业提示词
|
||||||
|
func BuildCommitMsgPrompt(lang string) string {
|
||||||
|
langName := "English"
|
||||||
|
exampleSummary := "feat(auth): add Redis token caching and fix mutex deadlock"
|
||||||
|
exampleBullet1 := "- Introduce Redis caching layer for user authentication"
|
||||||
|
exampleBullet2 := "- Fix concurrency bug where mutex was not unlocked on early return"
|
||||||
|
|
||||||
|
switch lang {
|
||||||
|
case "zh":
|
||||||
|
langName = "简体中文 (Simplified Chinese)"
|
||||||
|
exampleSummary = "feat(auth): 引入 Redis 缓存并修复 Mutex 死锁隐患"
|
||||||
|
exampleBullet1 = "- 在用户认证模块新增 Redis Token 缓存机制"
|
||||||
|
exampleBullet2 = "- 修复提前 return 时未释放互斥锁的并发隐患"
|
||||||
|
case "ja":
|
||||||
|
langName = "日本語 (Japanese)"
|
||||||
|
exampleSummary = "feat(auth): Redisキャッシュを追加し、Mutexのデッドロックを修正"
|
||||||
|
exampleBullet1 = "- ユーザー認証ハンドラーに Redis トークンキャッシュ層を導入"
|
||||||
|
exampleBullet2 = "- 早期リターン時に Mutex の Unlock が漏れる並行処理のバグを修正"
|
||||||
|
case "ko":
|
||||||
|
langName = "한국어 (Korean)"
|
||||||
|
exampleSummary = "feat(auth): Redis 캐시 도입 및 Mutex 데드락 버그 수정"
|
||||||
|
exampleBullet1 = "- 사용자 인증 핸들러에 Redis 토큰 캐시 계층 추가"
|
||||||
|
exampleBullet2 = "- 조기 반환 시 Mutex가 해제되지 않는 동시성 문제 해결"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`你是一位精通 Git 版本控制与 Conventional Commits 规范的资深架构师。
|
||||||
|
你的任务是根据开发者提供的 Git Diff 增量代码,生成一条极其规范、精炼、清晰的 Git Commit Message。
|
||||||
|
|
||||||
|
### 规则要求:
|
||||||
|
1. 必须使用 Conventional Commits 规范:<type>(<scope>): <subject>
|
||||||
|
- type 可选:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
||||||
|
- scope 为本次变动的核心模块或文件名(英文小写,如 auth, db, ui, config 等)
|
||||||
|
2. 【语言要求】:除 type(scope) 以外的主题与说明必须使用【%s】撰写!
|
||||||
|
3. 输出结构:
|
||||||
|
- 第一行:精炼的主题行(不超过 60 个字符,不带句号)
|
||||||
|
- 第二行:留空行
|
||||||
|
- 第三行及之后(可选):使用 1~3 个无序列表项 (-) 简述核心改动要点
|
||||||
|
4. 严禁任何 Markdown 代码块包裹,严禁任何客套话或多余解释,直接输出纯文本 Commit Message!
|
||||||
|
|
||||||
|
### 示例参考:
|
||||||
|
%s
|
||||||
|
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
`, langName, exampleSummary, exampleBullet1, exampleBullet2)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package reviewer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDetectLanguage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"zh", "zh"},
|
||||||
|
{"ZH", "zh"},
|
||||||
|
{"zh-CN", "zh"},
|
||||||
|
{"中文", "zh"},
|
||||||
|
{"ja", "ja"},
|
||||||
|
{"ja-JP", "ja"},
|
||||||
|
{"Japanese", "ja"},
|
||||||
|
{"日本語", "ja"},
|
||||||
|
{"ko", "ko"},
|
||||||
|
{"ko-KR", "ko"},
|
||||||
|
{"한국어", "ko"},
|
||||||
|
{"en", "en"},
|
||||||
|
{"en-US", "en"},
|
||||||
|
{"English", "en"},
|
||||||
|
{"fr", "en"}, // 未预置语言回退到 en
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := DetectLanguage(tt.input)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("DetectLanguage(%q) = %q, expected %q", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCommitMsgPrompt(t *testing.T) {
|
||||||
|
langs := []struct {
|
||||||
|
lang string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"zh", "简体中文"},
|
||||||
|
{"ja", "日本語"},
|
||||||
|
{"ko", "한국어"},
|
||||||
|
{"en", "English"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range langs {
|
||||||
|
prompt := BuildCommitMsgPrompt(tt.lang)
|
||||||
|
if !strings.Contains(prompt, tt.expected) {
|
||||||
|
t.Errorf("BuildCommitMsgPrompt(%q) 应包含语言名称 %q", tt.lang, tt.expected)
|
||||||
|
}
|
||||||
|
if !strings.Contains(prompt, "Conventional Commits") {
|
||||||
|
t.Errorf("BuildCommitMsgPrompt(%q) 应包含 Conventional Commits 说明", tt.lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-19
@@ -5,29 +5,43 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// DefaultBaseSystemPrompt 默认内置的资深架构师评审准则
|
// DefaultBaseSystemPrompt 默认内置的资深架构师评审准则
|
||||||
const DefaultBaseSystemPrompt = `你是一位拥有 15 年高并发系统设计经验的资深架构师与代码安全专家。
|
const DefaultBaseSystemPrompt = `你是一位拥有 15 年以上高可用架构、高并发底层设计与代码安全审计经验的顶级首席架构师(Principal Engineer)。
|
||||||
你的任务是对开发者提交的 Git Diff 增量代码进行工业级、严苛、精炼的代码评审(Code Review)。
|
你的核心使命是对开发者提交的 Git Diff 增量代码进行工业级、严苛、实证化、高信噪比的代码审查(Code Review)。
|
||||||
### 评审原则:
|
### 一、 核心审查原则(铁律)
|
||||||
1. 严禁任何客套话与无意义吹捧,直奔技术实质。
|
1. 【零客套、纯硬核】:严禁任何寒暄、恭维或废话,直接输出技术审查实质。
|
||||||
2. 聚焦于逻辑缺陷、并发竞态、安全漏洞、内存与协程泄漏、异常处理等实质性问题,不要纠结纯粹的代码缩进或格式。
|
2. 【实证主义与防幻觉】:指出缺陷时,必须明确指出【触发路径/前置条件】(如:“当 input 为空且缓存未命中时…”),严禁脱离 Diff 凭空臆测未改动外部逻辑。
|
||||||
3. 发现具体问题时,必须直接给出修改前后的对比例子或修复代码块。
|
3. 【最小侵入修复】:给出的修复方案必须语法正确、开箱即用,优先给出最小改动补丁,严禁使用包含大量省略号的伪代码。
|
||||||
4. 如果代码质量优秀且无缺陷,直接返回 "LGTM: 代码质量良好,未发现明显缺陷",不要强行挑刺。
|
4. 【无缺陷免打扰】:若本次变更逻辑严谨且无隐患,直接输出 "#### 评审结论\n- 结论: [PASS]\n- 评分: 95 / 100\nLGTM: 代码质量良好,未发现阻断性与潜在缺陷。"。
|
||||||
### 重点审查维度:
|
### 二、 缺陷严重级别定义(严格执行,不得随意跨级)
|
||||||
- 并发与竞态安全:Goroutine 泄露、Channel 阻塞、锁竞争与未释放、共享变量无锁并发读写。
|
- [BLOCKER] (阻断级/必须拦截):
|
||||||
- 安全与权限:SQL 注入风险、敏感密钥/Token 硬编码、未校验的外部输入、未做权限校验。
|
- 必然或高概率导致:线上 Panic/Crash、死锁/活锁、Goroutine/内存泄露、并发竞态读写破坏、SQL 注入/越权/密钥硬编码、事务脏读/数据丢失、未释放资源句柄。
|
||||||
- 资源与错误处理:连接/文件未 defer Close()、错误被盲目下划线 _ 忽略、Context 未向后传递。
|
- 【结论强制为 [REJECT],评分 < 60】
|
||||||
- 性能与边界:大循环内的频繁内存分配或数据库查询、切片越界、空指针异常(NIL Dereference)。
|
- [WARNING] (告警级/潜在隐患):
|
||||||
### 输出格式规范(使用清晰严谨的纯文本标签):
|
- 潜在风险或性能退化:缺乏超时控制/重试风暴、大循环内频繁堆分配/N+1 查询、静默吞掉底层 Error、边界条件未覆盖、缺少 defer 兜底。
|
||||||
|
- 【结论为 [WARN],评分 60 ~ 85】
|
||||||
|
- [SUGGESTION] (优化级/可读性与习惯):
|
||||||
|
- 不影响逻辑正确性:更优雅的语言原生 Idiomatic 写法、冗余代码精简、命名规范。
|
||||||
|
- 【结论为 [PASS],评分 85 ~ 100】
|
||||||
|
### 三、 重点技术维度专项排查清单
|
||||||
|
1. 并发与竞态:Goroutine 无退出机制(泄露)、Channel 永久阻塞、Mutex 跨函数未释放、共享变量无原子/锁保护并发读写、sync.WaitGroup 传值而非指针。
|
||||||
|
2. 资源与内存:文件/连接/DB rows 未立即 defer Close()、time.After 在循环/select 中堆积内存、大切片底层数组无法 GC。
|
||||||
|
3. 异常与边界:下划线 _ 忽略关键 error、多层调用缺少 Context 传递/超时、空指针解引用(Nil Dereference)、数组/切片越界。
|
||||||
|
4. 安全与权限:动态拼接 SQL 语句、未转义外部输入、硬编码敏感凭证、缺少幂等性保障。
|
||||||
|
### 四、 输出格式规范(必须严格遵循 Markdown 格式)
|
||||||
#### 变更概述
|
#### 变更概述
|
||||||
(用 1-2 句话总结本次代码改动核心内容)
|
(用 1-2 句话客观概括本次变更的业务逻辑与技术影响)
|
||||||
#### 详细审查意见
|
#### 详细审查意见
|
||||||
(如果有问题,按严重程度列出;若无问题则省略此段)
|
(若存在问题,按严重程度递减排列;若无缺陷则此段直接省略)
|
||||||
- [BLOCKER] 文件路径:行号 - 问题描述及潜在事故危害(必须附带修复代码块)
|
- [BLOCKER] 文件路径:行号 - 缺陷描述:说明触发场景、事故危害及调用路径
|
||||||
- [WARNING] 文件路径:行号 - 潜在隐患或性能优化点
|
// 建议修复代码:
|
||||||
- [SUGGESTION] 文件路径:行号 - 更优雅的 Idiomatic 写法
|
` + "```go" + `
|
||||||
|
// 修复后的代码块
|
||||||
|
` + "```" + `
|
||||||
|
- [WARNING] 文件路径:行号 - 潜在隐患:说明风险点及性能优化建议
|
||||||
|
- [SUGGESTION] 文件路径:行号 - 优化建议:更地道的代码组织方式
|
||||||
#### 评审结论
|
#### 评审结论
|
||||||
- 结论: [PASS / WARN / REJECT]
|
- 结论: [PASS / WARN / REJECT]
|
||||||
- 评分: X / 100
|
- 评分: X / 100 (0-100分)
|
||||||
`
|
`
|
||||||
|
|
||||||
// BuildSystemPrompt 构建最终 Prompt(合并 Base Prompt 与 rules.go 加载的项目规则)
|
// BuildSystemPrompt 构建最终 Prompt(合并 Base Prompt 与 rules.go 加载的项目规则)
|
||||||
|
|||||||
@@ -40,3 +40,25 @@ func (r *Reviewer) Execute(ctx context.Context, diff string, stream bool, onChun
|
|||||||
result, err := r.client.Review(ctx, systemPrompt, diff)
|
result, err := r.client.Review(ctx, systemPrompt, diff)
|
||||||
return result, nil, err
|
return result, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateCommitMsg 根据 Diff 智能生成符合 Conventional Commits 规范的提交信息
|
||||||
|
func (r *Reviewer) GenerateCommitMsg(ctx context.Context, diff string, lang string) (string, error) {
|
||||||
|
if strings.TrimSpace(diff) == "" {
|
||||||
|
return "", fmt.Errorf("diff 内容为空,无法生成 Commit Message")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetLang := DetectLanguage(lang)
|
||||||
|
systemPrompt := BuildCommitMsgPrompt(targetLang)
|
||||||
|
|
||||||
|
msg, err := r.client.Review(ctx, systemPrompt, diff)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理多余的代码块标记或前后空白
|
||||||
|
cleanMsg := strings.TrimSpace(msg)
|
||||||
|
cleanMsg = strings.TrimPrefix(cleanMsg, "```gitcommit")
|
||||||
|
cleanMsg = strings.TrimPrefix(cleanMsg, "```")
|
||||||
|
cleanMsg = strings.TrimSuffix(cleanMsg, "```")
|
||||||
|
return strings.TrimSpace(cleanMsg), nil
|
||||||
|
}
|
||||||
@@ -52,6 +52,19 @@ func PrintUsage(inputTokens, outputTokens int, costUSD float64) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrintUpdateBanner 打印新版本升级提醒卡片
|
||||||
|
func PrintUpdateBanner(currentVer, latestVer, releaseURL string) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(colorYellow + "┌─────────────────────────────────────────────────────────────┐" + colorReset)
|
||||||
|
fmt.Printf(colorYellow+"│"+colorReset+" 🚀 %sgit-air 新版本发布: %s%s%s → %s%s%s\n",
|
||||||
|
colorBold, colorGray, currentVer, colorReset,
|
||||||
|
colorGreen, latestVer, colorReset,
|
||||||
|
)
|
||||||
|
fmt.Println(colorYellow + "│" + colorReset + " 运行以下命令一键升级: " + colorYellow + "│" + colorReset)
|
||||||
|
fmt.Printf(colorYellow+"│"+colorReset+" %sgo install github.com/unclesam-ly/git-air@latest%s "+colorYellow+"│"+colorReset+"\n", colorCyan, colorReset)
|
||||||
|
fmt.Println(colorYellow + "└─────────────────────────────────────────────────────────────┘" + colorReset)
|
||||||
|
}
|
||||||
|
|
||||||
// PrintError 打印错误信息
|
// PrintError 打印错误信息
|
||||||
func PrintError(format string, a ...interface{}) {
|
func PrintError(format string, a ...interface{}) {
|
||||||
msg := fmt.Sprintf(format, a...)
|
msg := fmt.Sprintf(format, a...)
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CurrentVersion 当前编译版本号
|
||||||
|
const CurrentVersion = "v1.0.5"
|
||||||
|
|
||||||
|
const (
|
||||||
|
githubAPIURL = "https://api.github.com/repos/unclesam-ly/git-air/releases/latest"
|
||||||
|
checkInterval = 24 * time.Hour
|
||||||
|
httpTimeout = 1500 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateState struct {
|
||||||
|
LastChecked int64 `json:"last_checked"`
|
||||||
|
LatestVer string `json:"latest_ver"`
|
||||||
|
ReleaseURL string `json:"release_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateInfo struct {
|
||||||
|
CurrentVersion string
|
||||||
|
LatestVersion string
|
||||||
|
ReleaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachePath() (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dir := filepath.Join(home, ".git-air")
|
||||||
|
_ = os.MkdirAll(dir, 0755)
|
||||||
|
return filepath.Join(dir, "update_check.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCache() (*UpdateState, error) {
|
||||||
|
path, err := getCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var state UpdateState
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCache(state *UpdateState) {
|
||||||
|
path, err := getCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(state)
|
||||||
|
if err == nil {
|
||||||
|
_ = os.WriteFile(path, data, 0600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUpdate 检查是否有新版本(带 24 小时本地缓存与极短超时,静默处理所有异常)
|
||||||
|
func CheckUpdate(force bool) *UpdateInfo {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
// 1. 如果不是强制检查,优先读取 24 小时内的本地缓存
|
||||||
|
if !force {
|
||||||
|
if state, err := readCache(); err == nil && state != nil {
|
||||||
|
if now-state.LastChecked < int64(checkInterval.Seconds()) {
|
||||||
|
if IsNewerVersion(state.LatestVer, CurrentVersion) {
|
||||||
|
return &UpdateInfo{
|
||||||
|
CurrentVersion: CurrentVersion,
|
||||||
|
LatestVersion: state.LatestVer,
|
||||||
|
ReleaseURL: state.ReleaseURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 发起轻量级 GitHub API 请求
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", githubAPIURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "git-air-cli")
|
||||||
|
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: httpTimeout}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil || resp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var release struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil || release.TagName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 写入本地缓存
|
||||||
|
writeCache(&UpdateState{
|
||||||
|
LastChecked: now,
|
||||||
|
LatestVer: release.TagName,
|
||||||
|
ReleaseURL: release.HTMLURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 比对版本
|
||||||
|
if IsNewerVersion(release.TagName, CurrentVersion) {
|
||||||
|
return &UpdateInfo{
|
||||||
|
CurrentVersion: CurrentVersion,
|
||||||
|
LatestVersion: release.TagName,
|
||||||
|
ReleaseURL: release.HTMLURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAsync 后台并发静默检查,返回 channel 供主流程结束前快速获取
|
||||||
|
func CheckAsync() <-chan *UpdateInfo {
|
||||||
|
ch := make(chan *UpdateInfo, 1)
|
||||||
|
go func() {
|
||||||
|
info := CheckUpdate(false)
|
||||||
|
ch <- info
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNewerVersion 语义化版本号比对 (如 "v1.0.5" > "v1.0.4")
|
||||||
|
func IsNewerVersion(latest, current string) bool {
|
||||||
|
latestClean := strings.TrimPrefix(strings.TrimSpace(latest), "v")
|
||||||
|
currentClean := strings.TrimPrefix(strings.TrimSpace(current), "v")
|
||||||
|
|
||||||
|
if latestClean == "" || currentClean == "" || latestClean == currentClean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
latestParts := strings.Split(latestClean, ".")
|
||||||
|
currentParts := strings.Split(currentClean, ".")
|
||||||
|
|
||||||
|
for i := 0; i < len(latestParts) && i < len(currentParts); i++ {
|
||||||
|
lNum, err1 := strconv.Atoi(latestParts[i])
|
||||||
|
cNum, err2 := strconv.Atoi(currentParts[i])
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
return latestClean > currentClean
|
||||||
|
}
|
||||||
|
if lNum > cNum {
|
||||||
|
return true
|
||||||
|
} else if lNum < cNum {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(latestParts) > len(currentParts)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsNewerVersion(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
latest string
|
||||||
|
current string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"v1.0.5", "v1.0.4", true},
|
||||||
|
{"v1.1.0", "v1.0.4", true},
|
||||||
|
{"v2.0.0", "v1.9.9", true},
|
||||||
|
{"v1.0.4", "v1.0.4", false},
|
||||||
|
{"v1.0.3", "v1.0.4", false},
|
||||||
|
{"v1.0.4.1", "v1.0.4", true},
|
||||||
|
{"1.0.5", "1.0.4", true},
|
||||||
|
{"", "v1.0.4", false},
|
||||||
|
{"v1.0.4", "", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := IsNewerVersion(tt.latest, tt.current)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("IsNewerVersion(%q, %q) = %v, expected %v", tt.latest, tt.current, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user