feat: 初始化 git-air AI 代码评审 CLI 工具
新增多模型 LLM 客户端、Git diff 提取与过滤、配置管理及 pre-commit 钩子支持。
This commit is contained in:
commit
55f40e015d
18 files changed
+1315
No files matched your search
+175
@@ -0,0 +1,175 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git-air/internal/ui"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func getConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir := filepath.Join(home, ".git-air")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "config.yaml"), nil
|
||||
}
|
||||
|
||||
// LoadConfig 读取全局配置(优先使用环境变量,其次读取 yaml 文件)
|
||||
func LoadConfig() (*AppConfig, error) {
|
||||
cfg := &AppConfig{
|
||||
Provider: "gemini",
|
||||
Model: "gemini-3.7-flash",
|
||||
}
|
||||
|
||||
// 1. 优先探测当前项目根目录下的配置文件
|
||||
localCandidates := []string{"config.yaml", ".git-air.yaml", ".git-air.yml", "configs/config.yaml"}
|
||||
loaded := false
|
||||
|
||||
for _, name := range localCandidates {
|
||||
if data, err := os.ReadFile(name); err == nil && len(data) > 0 {
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||
}
|
||||
loaded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果项目根目录下没有,则回退读取用户全局配置 ~/.git-air/config.yaml
|
||||
if !loaded {
|
||||
configPath, err := getConfigPath()
|
||||
if err == nil {
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
_ = yaml.Unmarshal(data, cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 环境变量最高优先级覆盖
|
||||
if envKey := os.Getenv("GIT_AIR_API_KEY"); envKey != "" {
|
||||
cfg.APIKey = envKey
|
||||
}
|
||||
if envProvider := os.Getenv("GIT_AIR_PROVIDER"); envProvider != "" {
|
||||
cfg.Provider = envProvider
|
||||
}
|
||||
if envModel := os.Getenv("GIT_AIR_MODEL"); envModel != "" {
|
||||
cfg.Model = envModel
|
||||
}
|
||||
if envBaseURL := os.Getenv("GIT_AIR_BASE_URL"); envBaseURL != "" {
|
||||
cfg.BaseURL = envBaseURL
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置到 ~/.git-air/config.yaml
|
||||
func SaveConfig(cfg *AppConfig) error {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(configPath, data, 0600)
|
||||
}
|
||||
|
||||
// configCmd 配置子命令
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "查看或修改 git-air 全局配置 (~/.git-air/config.yaml)",
|
||||
}
|
||||
var configSetCmd = &cobra.Command{
|
||||
Use: "set",
|
||||
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 --provider ollama --model qwen2.5-coder`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil || cfg == nil {
|
||||
cfg = &AppConfig{
|
||||
Provider: "gemini",
|
||||
Model: "gemini-3.7-flash",
|
||||
}
|
||||
}
|
||||
if p, _ := cmd.Flags().GetString("provider"); p != "" {
|
||||
cfg.Provider = p
|
||||
}
|
||||
if k, _ := cmd.Flags().GetString("key"); k != "" {
|
||||
cfg.APIKey = k
|
||||
}
|
||||
if m, _ := cmd.Flags().GetString("model"); m != "" {
|
||||
cfg.Model = m
|
||||
}
|
||||
if u, _ := cmd.Flags().GetString("base-url"); u != "" {
|
||||
cfg.BaseURL = u
|
||||
}
|
||||
if pr, _ := cmd.Flags().GetString("prompt"); pr != "" {
|
||||
cfg.CustomPrompt = pr
|
||||
}
|
||||
if err := SaveConfig(cfg); err != nil {
|
||||
ui.PrintError("保存配置失败: %v", err)
|
||||
return
|
||||
}
|
||||
ui.PrintSuccess("配置更新成功!")
|
||||
},
|
||||
}
|
||||
|
||||
var configGetCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "查看当前所有配置",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil || cfg == nil {
|
||||
cfg = &AppConfig{
|
||||
Provider: "gemini",
|
||||
Model: "gemini-3.7-flash",
|
||||
}
|
||||
}
|
||||
path, _ := getConfigPath()
|
||||
fmt.Printf("配置文件路径: %s\n\n", path)
|
||||
fmt.Printf("Provider: %s\n", cfg.Provider)
|
||||
fmt.Printf("Model: %s\n", cfg.Model)
|
||||
fmt.Printf("BaseURL: %s\n", cfg.BaseURL)
|
||||
maskedKey := "未配置"
|
||||
if len(cfg.APIKey) > 8 {
|
||||
maskedKey = cfg.APIKey[:4] + "..." + cfg.APIKey[len(cfg.APIKey)-4:]
|
||||
}
|
||||
fmt.Printf("APIKey: %s\n", maskedKey)
|
||||
hasPrompt := "否 (使用默认内置架构师 Prompt)"
|
||||
if cfg.CustomPrompt != "" {
|
||||
hasPrompt = "是 (已自定义)"
|
||||
}
|
||||
fmt.Printf("CustomPrompt: %s\n", hasPrompt)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
configSetCmd.Flags().StringP("provider", "p", "", "模型提供商 (gemini, claude, grok, deepseek, qwen, zhipu, moonshot, siliconflow, doubao, minimax, yi, groq, openrouter, ollama, openai)")
|
||||
configSetCmd.Flags().StringP("key", "k", "", "API 密钥")
|
||||
configSetCmd.Flags().StringP("model", "m", "", "模型名称")
|
||||
configSetCmd.Flags().StringP("base-url", "u", "", "自定义 API 地址")
|
||||
configSetCmd.Flags().String("prompt", "", "自定义 Base System Prompt")
|
||||
|
||||
configCmd.AddCommand(configSetCmd)
|
||||
configCmd.AddCommand(configGetCmd)
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"git-air/internal/git"
|
||||
"git-air/internal/ui"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const preCommitScript = `#!/bin/sh
|
||||
# git-air pre-commit hook
|
||||
echo "[git-air] 正在执行 Commit 前代码自动化评审..."
|
||||
git-air --cached
|
||||
# 如果评审发现阻断性问题,可通过退出码阻断 commit
|
||||
`
|
||||
|
||||
var hookCmd = &cobra.Command{
|
||||
Use: "hook",
|
||||
Short: "管理 Git Pre-commit 自动审查钩子",
|
||||
}
|
||||
|
||||
var hookInstallCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "在当前仓库一键安装 pre-commit 钩子",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
repoRoot, err := git.GetRepoRoot(cmd.Context())
|
||||
if err != nil {
|
||||
ui.PrintError("请在 Git 仓库根目录下执行此命令")
|
||||
return
|
||||
}
|
||||
hookPath := filepath.Join(repoRoot, ".git", "hooks", "pre-commit")
|
||||
if err := os.WriteFile(hookPath, []byte(preCommitScript), 0755); err != nil {
|
||||
ui.PrintError("写入 hook 文件失败: %v", err)
|
||||
return
|
||||
}
|
||||
ui.PrintSuccess("已成功安装 pre-commit 钩子!每次执行 git commit 时将自动触发评审。")
|
||||
},
|
||||
}
|
||||
|
||||
var hookUninstallCmd = &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "移除当前仓库的 pre-commit 钩子",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
repoRoot, err := git.GetRepoRoot(cmd.Context())
|
||||
if err != nil {
|
||||
ui.PrintError("请在 Git 仓库根目录下执行此命令")
|
||||
return
|
||||
}
|
||||
hookPath := filepath.Join(repoRoot, ".git", "hooks", "pre-commit")
|
||||
_ = os.Remove(hookPath)
|
||||
ui.PrintSuccess("已成功卸载 pre-commit 钩子!")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
hookCmd.AddCommand(hookInstallCmd)
|
||||
hookCmd.AddCommand(hookUninstallCmd)
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git-air/internal/git"
|
||||
"git-air/internal/llm"
|
||||
"git-air/internal/reviewer"
|
||||
"git-air/internal/ui"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "git-air [git diff 参数,如 HEAD~1, main..feature, 文件名等]",
|
||||
Short: "git-air: 极轻量、极速的 Git 原生 AI 代码评审工具",
|
||||
Long: `git-air 是一个纯 Go 编写的 Git AI 评审利器。
|
||||
支持一键评审暂存区、指定 Commit、分支差异或单文件,无缝兼容 Gemini、DeepSeek、Ollama 和 OpenAI。`,
|
||||
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
|
||||
}
|
||||
|
||||
// 允许命令行临时覆盖模型/Key
|
||||
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,
|
||||
})
|
||||
if err != nil {
|
||||
ui.PrintError("初始化模型失败: %v (提示: 请先执行 'git air config set --key YOUR_KEY')", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 获取仓库根目录 (用于加载 .airules)
|
||||
repoRoot, _ := git.GetRepoRoot(ctx)
|
||||
|
||||
// 5. 初始化评审引擎
|
||||
customPrompt, _ := cmd.Flags().GetString("prompt")
|
||||
if customPrompt == "" {
|
||||
customPrompt = cfg.CustomPrompt
|
||||
}
|
||||
rev := reviewer.NewReviewer(llmClient, repoRoot, customPrompt)
|
||||
|
||||
// 6. 打印开场横幅并开始流式评审
|
||||
ui.PrintBanner(cfg.Provider, cfg.Model)
|
||||
printer := ui.NewStreamPrinter()
|
||||
_, err = rev.Execute(ctx, diff, true, func(chunk string) {
|
||||
printer.PrintChunk(chunk)
|
||||
})
|
||||
if err != nil {
|
||||
ui.PrintError("\n评审过程中发生错误: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ui.PrintFooter()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configCmd)
|
||||
rootCmd.AddCommand(hookCmd)
|
||||
|
||||
rootCmd.Flags().StringP("key", "k", "", "临时指定 API Key")
|
||||
rootCmd.Flags().StringP("model", "m", "", "临时指定模型名称")
|
||||
rootCmd.Flags().StringP("provider", "p", "", "临时指定 Provider (gemini, deepseek, ollama)")
|
||||
rootCmd.Flags().String("prompt", "", "临时指定 System Prompt")
|
||||
}
|
||||
Reference in new issue
Block a user