feat(cli): 新增 AI 智能 Commit Message 生成与多语言支持

- 新增 `git air msg` 命令,支持根据暂存区或历史 Diff 自动生成 Conventional Commits 提交说明
- 支持中、英、日、韩多语言自适应及通过 CLI/配置文件/环境变量进行定制
- 完善全局配置体系,新增 `commit_lang` 选项及相关单元测试
This commit is contained in:
unclesam committed 2026-08-22 16:54:44 +08:00
1 parent 4d9674831b
commit 6eda653db6
9 files changed
+456 -40

No files matched your search

+29 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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 (謗ィ螂ィ: 鬮倬€溘�菴弱さ繧ケ繝�)
+21 -7
View File
@@ -12,13 +12,14 @@ import (
// AppConfig 全局配置结构体
type AppConfig struct {
Provider string `yaml:"provider"` // gemini, deepseek, ollama, openai, custom
APIKey string `yaml:"api_key"` // API Key
BaseURL string `yaml:"base_url"` // 自定义请求地址
Model string `yaml:"model"` // 模型名称
CustomPrompt string `yaml:"custom_prompt"` // 用户自定义的 Base System Prompt
PriceInput float64 `yaml:"price_input,omitempty"` // 自定义输入单价 (每 1M tokens 美元,可选)
PriceOutput float64 `yaml:"price_output,omitempty"` // 自定义输出单价 (每 1M tokens 美元,可选)
Provider string `yaml:"provider"` // gemini, deepseek, ollama, openai, custom
APIKey string `yaml:"api_key"` // API Key
BaseURL string `yaml:"base_url"` // 自定义请求地址
Model string `yaml:"model"` // 模型名称
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
View File
@@ -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)
}
+3
View File
@@ -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"
+97
View File
@@ -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)
}
+58
View File
@@ -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)
}
}
}
+22
View File
@@ -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
}