feat(cli): 新增 AI 智能 Commit Message 生成与多语言支持
- 新增 `git air msg` 命令,支持根据暂存区或历史 Diff 自动生成 Conventional Commits 提交说明 - 支持中、英、日、韩多语言自适应及通过 CLI/配置文件/环境变量进行定制 - 完善全局配置体系,新增 `commit_lang` 选项及相关单元测试
This commit is contained in:
1 parent
4d9674831b
commit
6eda653db6
9 files changed
+449
-33
No files matched your search
@@ -49,6 +49,7 @@
|
||||
- 🛡️ **智能降噪过滤**:自动剥离 `go.sum`、`package-lock.json`、`*.pb.go`、`*.gen.go` 等锁文件与自动生成代码,省 Token、防死循环。
|
||||
- 🎯 **严谨架构师准则**:直奔主题,聚焦并发竞态、死锁、SQL 注入、空指针、资源泄露,并直接提供修复参考代码。
|
||||
- 📊 **Token 消耗与费用实时估算**:实时捕获并统计输入/输出 Token 消耗,内置 2026 最新官方定价表精准核算每次审查成本(本地离线模型自动标注免费)。
|
||||
- ✨ **AI 智能 Commit Message**:一键根据代码变更生成规范的 Conventional Commits 提交说明,支持**中/英/日/韩**等多语言自适应。
|
||||
- 📋 **团队规范适配(`.airules`)**:在项目根目录放置 `.airules`,AI 自动加载团队专属架构规范并最高优先级执行。
|
||||
- 🪝 **一键 Pre-commit 钩子**:一行命令挂载 Git 提交前自动拦截,在代码上库前把好第一道质量关。
|
||||
|
||||
@@ -175,7 +176,28 @@ 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 air msg -l en
|
||||
|
||||
# 3. 生成日语 / 韩语提交说明
|
||||
git air msg -l ja
|
||||
git air msg -l ko
|
||||
|
||||
# 4. 生成后无需确认直接执行 git commit
|
||||
git air msg -c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 一键安装 Git Pre-commit 拦截钩子
|
||||
|
||||
在你的 Git 项目根目录下执行:
|
||||
```bash
|
||||
@@ -195,8 +217,8 @@ git air hook uninstall
|
||||
## ⚙️ 配置文件说明
|
||||
|
||||
`git-air` 支持多层配置覆盖,优先级从高到低为:
|
||||
1. **命令行 Flags**(如 `--key`, `--model`, `--provider`, `--strict`, `--no-block`)
|
||||
2. **环境变量**(`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
||||
1. **命令行 Flags**(如 `--key`, `--model`, `--provider`, `--lang`, `--strict`, `--no-block`)
|
||||
2. **环境变量**(`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`, `GIT_AIR_COMMIT_LANG`)
|
||||
3. **项目级配置**(当前 Git 项目根目录下的 `config.yaml` 或 `.git-air.yaml`)
|
||||
4. **全局用户配置**(`~/.git-air/config.yaml`)
|
||||
|
||||
@@ -217,9 +239,12 @@ model: "gemini-3.7-flash"
|
||||
# 自定义基础提示词 (可选,留空则使用默认资深架构师准则)
|
||||
# custom_prompt: ""
|
||||
|
||||
# 自定义模型单价 (可选,单位: 美元/1M Tokens,用于精准核算自定义中转或企业私有接口费用)
|
||||
# 自定义模型单价 (可选,单位: 美元/1M Tokens)
|
||||
# price_input: 0.75
|
||||
# price_output: 3.75
|
||||
|
||||
# AI Commit Message 生成语言 (可选: auto, zh, en, ja, ko)
|
||||
# commit_lang: "auto"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+26
-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.
|
||||
- 🎯 **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).
|
||||
- ✨ **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.
|
||||
- 🪝 **One-Click Pre-commit Hook**: Mounts as a local Git hook in one command to block dangerous bugs before code is committed.
|
||||
|
||||
@@ -174,7 +175,26 @@ 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 staged changes and crafts a clean Conventional Commits message with **automatic language detection**:
|
||||
|
||||
```bash
|
||||
# 1. Generate based on your system locale & interactively commit
|
||||
git air msg
|
||||
|
||||
# 2. Explicitly specify target language (e.g. English, Japanese, Korean)
|
||||
git air msg -l en
|
||||
git air msg -l ja
|
||||
git air msg -l ko
|
||||
|
||||
# 3. Generate and directly commit without confirmation
|
||||
git air msg -c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. One-Click Git Pre-commit Hook
|
||||
|
||||
Install the hook inside any Git repository:
|
||||
```bash
|
||||
@@ -194,8 +214,8 @@ git air hook uninstall
|
||||
## ⚙️ Configuration Hierarchy
|
||||
|
||||
`git-air` searches for configurations in the following priority order:
|
||||
1. **CLI Flags** (`--key`, `--model`, `--provider`, `--prompt`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||
2. **Environment Variables** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
||||
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`, `GIT_AIR_COMMIT_LANG`)
|
||||
3. **Project Config** (`./config.yaml` or `./.git-air.yaml` in repo root)
|
||||
4. **Global User Config** (`~/.git-air/config.yaml`)
|
||||
|
||||
@@ -215,6 +235,9 @@ model: "gemini-3.7-flash"
|
||||
# Custom Token Pricing (Optional, in USD per 1M tokens)
|
||||
# price_input: 0.75
|
||||
# price_output: 3.75
|
||||
|
||||
# AI Commit Message Language (Optional: auto, zh, en, ja, ko)
|
||||
# commit_lang: "auto"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+61
-26
@@ -48,6 +48,7 @@ GitHubなどのCI/CD上で動作するボットも優秀ですが、**`git-air`
|
||||
- �孱�� **繧ケ繝槭�繝医↑繝弱う繧コ髯、蜴サ**�啻go.sum` 繧� `package-lock.json`縲∬�蜍慕函謌舌&繧後◆ `*.pb.go` 繧定�蜍暮勁螟悶5oken豸郁イサ繧呈椛縺医∵晁�Ν繝シ繝励r髦イ縺弱∪縺吶�
|
||||
- �識 **蜴ウ譬シ縺ェ繧キ繝九い繧ィ繝ウ繧ク繝九い蝓コ貅�**�壹♀荳冶セ槭d辟。鬧�哨縺ッ荳蛻�↑縺励ゆクヲ陦悟�逅��遶カ蜷医√ョ繝�ラ繝ュ繝�け縲ヾQL繧、繝ウ繧ク繧ァ繧ッ繧キ繝ァ繝ウ縲¨ULL繝昴う繝ウ繧ソ縲√Μ繧ス繝シ繧ケ繝ェ繝シ繧ッ繧呈欠鞫倥@縲∝�菴鍋噪縺ェ菫ョ豁」繧ウ繝シ繝峨r逶エ謗・謠千、コ縺励∪縺吶�
|
||||
- �投 **Token豸郁イサ驥上→雋サ逕ィ縺ョ繝ェ繧「繝ォ繧ソ繧、繝�隕狗ゥ阪b繧�**�壼�蜃コ蜉婬oken謨ー繧呈ュ」遒コ縺ォ髮�ィ医@縲∝推遉セ譛譁ー縺ョ蜈ャ蠑乗侭驥題。ィ縺ォ蝓コ縺・縺�※螳溯。後さ繧ケ繝医r蜊ウ蠎ァ縺ォ險育ョ暦シ医Ο繝シ繧ォ繝ォ螳悟�繧ェ繝輔Λ繧、繝ウ繝「繝�Ν縺ッ辟。譁呵。ィ遉コ�峨�
|
||||
- 笨ィ **AI 繧ウ繝溘ャ繝医Γ繝�そ繝シ繧ク閾ェ蜍慕函謌�**�壼、画峩蟾ョ蛻�°繧� Conventional Commits 貅匁侠縺ョ鬮伜刀雉ェ縺ェ繝。繝�そ繝シ繧ク繧堤函謌撰シ�**譌・譛ャ隱槭�闍ア隱槭�髻灘嵜隱槭�荳ュ蝗ス隱�**縺ォ閾ェ蜍暮←蠢懶シ峨�
|
||||
- �搭 **繝√�繝�隕冗エ��隱ュ縺ソ霎シ縺ソ��.airules`��**�壹Μ繝昴ず繝医Μ逶エ荳九↓ `.airules` 繧帝�鄂ョ縺吶k縺�縺代〒縲√メ繝シ繝�蝗コ譛峨�繧「繝シ繧ュ繝�け繝√Ε隕冗エ�r譛蜆ェ蜈医〒驕ゥ逕ィ縲�
|
||||
- �ェ� **繝ッ繝ウ繧ッ繝ェ繝�け縺ァ Pre-commit 繝輔ャ繧ッ蛹�**�壹さ繝槭Φ繝�1逋コ縺ァGit繝輔ャ繧ッ縺ィ縺励※逋サ骭イ縲ょ些髯コ縺ェ繧ウ繝シ繝峨�繧ウ繝溘ャ繝医r譛ェ辟カ縺ォ繝悶Ο繝�け縺励∪縺吶�
|
||||
|
||||
@@ -64,46 +65,80 @@ $ git air
|
||||
繝ヲ繝シ繧カ繝シ隱崎ィシ繝上Φ繝峨Λ繝シ縺ォ Redis 繧ュ繝」繝�す繝・螻、繧定ソス蜉�縺励,ontext 縺ョ繧ソ繧、繝�繧「繧ヲ繝井シ晄眺繧剃ソョ豁」縲�
|
||||
|
||||
#### 隧ウ邏ー繝ャ繝薙Η繝シ邨先棡
|
||||
- [BLOCKER] internal/service/user.go:45 - 重大なセキュリティ脆弱性: ループ内で生SQL文字列を直接結合しています。SQLインジェクションのリスクがあります。
|
||||
// 推奨修正コード:
|
||||
db.Where("username = ?", inputName).First(&user)
|
||||
- [BLOCKER] internal/service/user.go:45 - 驥榊、ァ縺ェ繧サ繧ュ繝・繝ェ繝�ぅ�# 迚ケ螳壹�繝輔ぃ繧、繝ォ縺ョ縺ソ繧偵Ξ繝薙Η繝シ
|
||||
git air internal/service/chat.go
|
||||
|
||||
- [WARNING] internal/service/user.go:82 - 潜在的リスク: Redis 接続エラーを握りつぶしています。キャッシュ障害時にDBへ負荷が集中します。
|
||||
|
||||
- [WARNING] internal/service/user.go:103 - 並行処理の欠陥: 早期リターン時に Mutex の Unlock を忘れています(Mutex Leak)。デッドロックの危険性があります。
|
||||
|
||||
#### 結論
|
||||
- 判定: [REJECT]
|
||||
- スコア: 60 / 100
|
||||
|
||||
📊 Token: 入力 1,243 / 出力 412 | ≈ $0.0025
|
||||
─────────────────────────────────────────────────────────────────
|
||||
# 荳譎ら噪縺ォ繝「繝�Ν繧Пrovider繧呈欠螳壹@縺ヲ螳溯。�
|
||||
git air --provider deepseek --model deepseek-chat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 インストール
|
||||
### 3. AI 繧ウ繝溘ャ繝医Γ繝�そ繝シ繧ク閾ェ蜍慕函謌� (`git air msg`)
|
||||
|
||||
### 方法 1: Go Install(推奨)
|
||||
```bash
|
||||
go install github.com/unclesam-ly/git-air@latest
|
||||
```
|
||||
繧ウ繝溘ャ繝医Γ繝�そ繝シ繧ク縺ョ菴懈�縺ォ謔ゥ繧蠢�ヲ√�繧ゅ≧縺ゅj縺セ縺帙s縲Agit-air` 縺後せ繝��繧ク繝ウ繧ー縺輔l縺溷キョ蛻�r蛻�梵縺励,onventional Commits 貅匁侠縺ョ繝。繝�そ繝シ繧ク繧�**螟夊ィ隱櫁�蜍暮←蠢�**縺ァ逕滓�縺励∪縺呻シ�
|
||||
|
||||
### 方法 2: ソースコードからビルド
|
||||
```bash
|
||||
git clone https://github.com/unclesam-ly/git-air.git
|
||||
cd git-air
|
||||
go build -o git-air .
|
||||
sudo mv git-air /usr/local/bin/
|
||||
# 1. 繧キ繧ケ繝�Β險隱槭r閾ェ蜍墓、懷�縺励※逕滓� & 蟇セ隧ア蝙九さ繝溘ャ繝�
|
||||
git air msg
|
||||
|
||||
# 2. 闍ア隱槭〒逕滓��医が繝シ繝励Φ繧ス繝シ繧ケ繝サ蝗ス髫帙�繝ュ繧ク繧ァ繧ッ繝亥髄縺托シ�
|
||||
git air msg -l en
|
||||
|
||||
# 3. 譌・譛ャ隱� / 髻灘嵜隱槭〒逕滓�
|
||||
git air msg -l ja
|
||||
git air msg -l ko
|
||||
|
||||
# 4. 遒コ隱阪↑縺励〒逶エ謗・ git commit 繧貞ョ溯。�
|
||||
git air msg -c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 クイックスタート
|
||||
### 4. 繝ッ繝ウ繧ソ繝�メ縺ァ Pre-commit 繝輔ャ繧ッ繧堤匳骭イ
|
||||
|
||||
### 1. モデルと API Key の設定
|
||||
Git 繝ェ繝昴ず繝医Μ逶エ荳九〒莉・荳九r螳溯。鯉シ�
|
||||
```bash
|
||||
git air hook install
|
||||
```
|
||||
- **繧ウ繝溘ャ繝郁�蜍輔ヶ繝ュ繝�け讖溯�**�壹Ξ繝薙Η繝シ縺ァ `[BLOCKER]` 驥榊、ァ谺�髯・繧� `[REJECT]` 蛻、螳壹′讀懷�縺輔l縺溷�エ蜷医~git-air` 縺ッ髱槭ぞ繝ュ縺ョ邨ゆコ�さ繝シ繝峨〒**繧ウ繝溘ャ繝茨シ�git commit`�峨r閾ェ蜍慕噪縺ォ荳ュ譁ュ繝サ繝悶Ο繝�け**縺励∪縺呻シ�
|
||||
- **蜴ウ譬シ繝「繝シ繝� (`--strict`)**�啻[WARNING]` 隴ヲ蜻翫〒繧ゅさ繝溘ャ繝医r繝悶Ο繝�け縺励◆縺��エ蜷医� `git air --strict` 繧剃スソ逕ィ��
|
||||
- **荳譎ら噪縺ェ繝悶Ο繝�け隗」髯、**�夂キ頑・縺ァ繧ウ繝溘ャ繝医r蠑キ蛻カ縺励◆縺��エ蜷医�縲;it 讓呎コ悶� `git commit --no-verify` 縺セ縺溘� `git air --no-block` 繧剃サ倅ク弱@縺セ縺吶�
|
||||
|
||||
`git-air` は国内外の主要LLMのエンドポイントと推奨モデルを標準プリセットしています。`--provider` を指定するだけで簡単に切り替えられます:
|
||||
繝輔ャ繧ッ繧定ァ」髯、縺吶k蝣エ蜷茨シ�
|
||||
```bash
|
||||
git air hook uninstall
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 笞呻ク� 險ュ螳壹�蜆ェ蜈磯��ス�
|
||||
|
||||
1. **繧ウ繝槭Φ繝峨Λ繧、繝ウ蠑墓焚** (`--key`, `--model`, `--provider`, `--prompt`, `--lang`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||
2. **迺ー蠅�、画焚** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`, `GIT_AIR_COMMIT_LANG`)
|
||||
3. **繝励Ο繧ク繧ァ繧ッ繝亥句挨險ュ螳�** (繝ェ繝昴ず繝医Μ逶エ荳九� `./config.yaml` 縺セ縺溘� `./.git-air.yaml`)
|
||||
4. **繧ー繝ュ繝シ繝舌Ν險ュ螳�** (`~/.git-air/config.yaml`)
|
||||
|
||||
### `config.yaml` 險ュ螳壹ヵ繧。繧、繝ォ萓具シ�
|
||||
```yaml
|
||||
# 繝励Ο繝舌う繝繝シ: gemini, claude, grok, deepseek, qwen, zhipu, moonshot, siliconflow, ollama, openai, custom
|
||||
provider: "gemini"
|
||||
api_key: "YOUR_API_KEY_HERE"
|
||||
model: "gemini-3.7-flash"
|
||||
|
||||
# 繧ォ繧ケ繧ソ繝� API 繧ィ繝ウ繝峨�繧、繝ウ繝� (莉サ諢�)
|
||||
# base_url: "https://api.deepseek.com/v1"
|
||||
|
||||
# 繧ォ繧ケ繧ソ繝� System Prompt (莉サ諢�)
|
||||
# custom_prompt: ""
|
||||
|
||||
# 繧ォ繧ケ繧ソ繝� Token 譁咎≡險ュ螳� (莉サ諢上∝腰菴�: 邀ウ繝峨Ν / 1M Tokens)
|
||||
# price_input: 0.75
|
||||
# price_output: 3.75
|
||||
|
||||
# AI 繧ウ繝溘ャ繝医Γ繝�そ繝シ繧ク險隱櫁ィュ螳� (莉サ諢�: auto, zh, en, ja, ko)
|
||||
# commit_lang: "auto"
|
||||
```LLM縺ョ繧ィ繝ウ繝峨�繧、繝ウ繝医→謗ィ螂ィ繝「繝�Ν繧呈ィ呎コ悶�繝ェ繧サ繝�ヨ縺励※縺�∪縺吶A--provider` 繧呈欠螳壹☆繧九□縺代〒邁。蜊倥↓蛻�j譖ソ縺医i繧後∪縺呻シ�
|
||||
|
||||
```bash
|
||||
# 1. Google Gemini (謗ィ螂ィ: 鬮倬溘�菴弱さ繧ケ繝�)
|
||||
|
||||
@@ -19,6 +19,7 @@ type AppConfig struct {
|
||||
CustomPrompt string `yaml:"custom_prompt"` // 用户自定义的 Base System Prompt
|
||||
PriceInput float64 `yaml:"price_input,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) {
|
||||
@@ -77,6 +78,9 @@ func LoadConfig() (*AppConfig, error) {
|
||||
if envBaseURL := os.Getenv("GIT_AIR_BASE_URL"); envBaseURL != "" {
|
||||
cfg.BaseURL = envBaseURL
|
||||
}
|
||||
if envLang := os.Getenv("GIT_AIR_COMMIT_LANG"); envLang != "" {
|
||||
cfg.CommitLang = envLang
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -104,6 +108,7 @@ var configSetCmd = &cobra.Command{
|
||||
Short: "设置全局配置项",
|
||||
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 --commit-lang ja
|
||||
git air config set --provider ollama --model qwen2.5-coder`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg, err := LoadConfig()
|
||||
@@ -134,6 +139,9 @@ var configSetCmd = &cobra.Command{
|
||||
if po, _ := cmd.Flags().GetFloat64("price-output"); po > 0 {
|
||||
cfg.PriceOutput = po
|
||||
}
|
||||
if cl, _ := cmd.Flags().GetString("commit-lang"); cl != "" {
|
||||
cfg.CommitLang = cl
|
||||
}
|
||||
if err := SaveConfig(cfg); err != nil {
|
||||
ui.PrintError("保存配置失败: %v", err)
|
||||
return
|
||||
@@ -168,6 +176,11 @@ var configGetCmd = &cobra.Command{
|
||||
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 {
|
||||
fmt.Printf("CustomPrice: 输入 $%.4f/1M, 输出 $%.4f/1M (已自定义覆盖)\n", cfg.PriceInput, cfg.PriceOutput)
|
||||
} else {
|
||||
@@ -184,6 +197,7 @@ func init() {
|
||||
configSetCmd.Flags().String("prompt", "", "自定义 Base System Prompt")
|
||||
configSetCmd.Flags().Float64("price-input", 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(configGetCmd)
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"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. 是否自动执行提交
|
||||
autoCommit, _ := cmd.Flags().GetBool("commit")
|
||||
if autoCommit {
|
||||
executeGitCommit(commitMsg)
|
||||
return
|
||||
}
|
||||
|
||||
// 交互式询问是否直接提交
|
||||
fmt.Print("? 是否直接以此信息执行 git commit?[Y/n]: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
|
||||
if input == "" || input == "y" || input == "yes" {
|
||||
executeGitCommit(commitMsg)
|
||||
} else {
|
||||
fmt.Println("\033[0;90m已跳过提交。你可以复制上方内容手动提交。\033[0m")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func executeGitCommit(msg string) {
|
||||
cmd := exec.Command("git", "commit", "-m", msg)
|
||||
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("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)
|
||||
}
|
||||
@@ -38,3 +38,6 @@ custom_prompt: ""
|
||||
# 若你的中转平台价格有变或使用私有计费,可在此自定义覆盖:
|
||||
# price_input: 0.75 # 每 1M 输入 Token 美元单价
|
||||
# price_output: 3.75 # 每 1M 输出 Token 美元单价
|
||||
|
||||
# AI Commit Message 生成语言 (可选,默认 "auto" 自动跟随系统语言,可选: zh, en, ja, ko)
|
||||
commit_lang: "auto"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,3 +40,25 @@ func (r *Reviewer) Execute(ctx context.Context, diff string, stream bool, onChun
|
||||
result, err := r.client.Review(ctx, systemPrompt, diff)
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user