Files
git-air/internal/git/diff.go
T
unclesam c7e8b449c9 feat(cmd): 优化提交命令的暂存区交互逻辑并支持上下文控制
- 新增 -a/--all 标志以支持自动暂存所有已修改的文件
- 增加对暂存区状态的检测,未暂存时提供友好的交互提示
- 为 Git 执行函数引入 context.Context 支持,确保超时与取消机制正常运作
2026-08-22 16:58:14 +08:00

120 lines
3.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package git
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
)
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) != "" {
return truncateDiffIfNeeded(FilterDiff(stagedDiff, customIgnores...)), nil
}
// 2. 如果暂存区为空,回退提取工作区未暂存的修改
cmdArgs = []string{}
} else {
cmdArgs = args
}
rawDiff, err := execGitDiff(ctx, cmdArgs...)
if err != nil {
return "", err
}
filteredDiff := FilterDiff(rawDiff, customIgnores...)
if strings.TrimSpace(filteredDiff) == "" {
return "", ErrEmptyDiff
}
return truncateDiffIfNeeded(filteredDiff), nil
}
func truncateDiffIfNeeded(diff string) string {
runes := []rune(diff)
if len(runes) > maxDiffSize {
runes = runes[:maxDiffSize]
}
return string(runes) + "\n\n[WARNING: 代码变更量过大,已自动截断前 300KB 内容进行评审]"
}
// 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
}