- 新增 custom_reviewer_prompt 和 custom_commit_msg_prompt,兼容旧 custom_prompt - pre-commit hook 安装时备份已有 hook,卸载时恢复 - 支持 --format json 输出评审结果 - diff 发送前自动脱敏敏感信息
125 lines
3.6 KiB
Go
125 lines
3.6 KiB
Go
package git
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"os/exec"
|
||
"strings"
|
||
|
||
"github.com/unclesam-ly/git-air/internal/security"
|
||
)
|
||
|
||
var (
|
||
ErrNotGitRepo = errors.New("当前目录不是 Git 仓库(未找到 .git)")
|
||
ErrEmptyDiff = errors.New("没有检测到任何需要评审的代码变更(Diff 为空)")
|
||
)
|
||
|
||
// IsGitRepo 检查当前工作目录是否在一个 Git 仓库内
|
||
func IsGitRepo(ctx context.Context) bool {
|
||
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--is-inside-work-tree")
|
||
err := cmd.Run()
|
||
return err == nil
|
||
}
|
||
|
||
// GetRepoRoot 获取当前 Git 仓库的根目录绝对路径
|
||
func GetRepoRoot(ctx context.Context) (string, error) {
|
||
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel")
|
||
out, err := cmd.Output()
|
||
if err != nil {
|
||
return "", ErrNotGitRepo
|
||
}
|
||
|
||
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
|
||
|
||
// GetDiff 提取并过滤 Git 代码差异
|
||
// args 可传:
|
||
// - 无参数:默认先查暂存区 (git diff --cached),若暂存区为空则自动查工作区 (git diff)
|
||
// - 自定义参数:如 "HEAD~1"、"main..feature"、"internal/service/chat.go" 等
|
||
func GetDiff(ctx context.Context, args ...string) (string, error) {
|
||
if !IsGitRepo(ctx) {
|
||
return "", ErrNotGitRepo
|
||
}
|
||
|
||
repoRoot, _ := GetRepoRoot(ctx)
|
||
customIgnores := LoadAirIgnore(repoRoot)
|
||
|
||
var cmdArgs []string
|
||
if len(args) == 0 {
|
||
// 1. 默认先提取暂存区代码 (准备 commit 的代码)
|
||
stagedDiff, err := execGitDiff(ctx, "--cached")
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if strings.TrimSpace(stagedDiff) != "" {
|
||
truncated := truncateDiffIfNeeded(FilterDiff(stagedDiff, customIgnores...))
|
||
return security.RedactSecrets(truncated), nil
|
||
}
|
||
// 2. 如果暂存区为空,回退提取工作区未暂存的修改
|
||
cmdArgs = []string{}
|
||
} else {
|
||
cmdArgs = args
|
||
}
|
||
|
||
rawDiff, err := execGitDiff(ctx, cmdArgs...)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
truncatedDiff := truncateDiffIfNeeded(FilterDiff(rawDiff, customIgnores...))
|
||
filteredDiff := security.RedactSecrets(truncatedDiff)
|
||
if strings.TrimSpace(filteredDiff) == "" {
|
||
return "", ErrEmptyDiff
|
||
}
|
||
|
||
return filteredDiff, nil
|
||
}
|
||
|
||
func truncateDiffIfNeeded(diff string) string {
|
||
runes := []rune(diff)
|
||
if len(runes) > maxDiffSize {
|
||
runes = runes[:maxDiffSize]
|
||
return string(runes) + "\n\n[WARNING: 代码变更量过大,已自动截断前 300KB 内容进行评审]"
|
||
}
|
||
return diff
|
||
}
|
||
|
||
// execGitDiff 执行底层的 git diff 命令
|
||
func execGitDiff(ctx context.Context, args ...string) (string, error) {
|
||
fullArgs := append([]string{"diff"}, args...)
|
||
cmd := exec.CommandContext(ctx, "git", fullArgs...)
|
||
var stdout, stderr bytes.Buffer
|
||
cmd.Stdout = &stdout
|
||
cmd.Stderr = &stderr
|
||
if err := cmd.Run(); err != nil {
|
||
if errors.Is(err, exec.ErrNotFound) {
|
||
return "", errors.New("系统未检测到 git 命令,请确保已安装 Git 并配置到系统 PATH 中")
|
||
}
|
||
errMsg := strings.TrimSpace(stderr.String())
|
||
if errMsg != "" {
|
||
return "", fmt.Errorf("git diff 执行失败: %s (%w)", errMsg, err)
|
||
}
|
||
return "", fmt.Errorf("git diff 执行失败: %w", err)
|
||
}
|
||
return stdout.String(), nil
|
||
}
|