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
+457
-41
No files matched your search
+21
-7
@@ -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
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user