// Package git wraps basic Git operations via direct exec.Command, // used by pkg/context to fill EnvInfo for system prompt injection // and by future sandbox pod self-checkout audit hooks (DESIGN.md // whitelist). pkg/memory/sync_git.go intentionally does NOT consume // this package — it goes through execenv.Executor DI (ClassMemoryGit) // for sandbox routing, keeping its own exec path. // // 包 git 通过裸 exec.Command 封装基础 Git 操作, 供 pkg/context // 填充 EnvInfo 注入系统提示词, 以及未来 sandbox M2 "pod 自身 // checkout 审计白名单" 场景消费. 注意 pkg/memory/sync_git.go // 刻意不走本包 — 它经 execenv.Executor DI (ClassMemoryGit) 执行, // 保留 sandbox-aware 路由, 和本包 exec 路径独立. package git import ( "fmt" "os/exec" "strconv" "strings" ) // Info describes the current git repository summary used by context's // EnvInfo to inject "Git repo / branch / status" into the system prompt. // // Info 描述当前 git 仓库摘要, 供 context.EnvInfo 注入系统提示词 // ("Git repo / branch / status" 字段). type Info struct { IsRepo bool // Whether cwd sits inside a git repo. / 是否在 git 仓库中. Branch string // Current branch name (empty on detached HEAD or non-repo). / 当前分支名 (detached HEAD 或非仓库时为空). Status string // git status --porcelain summary (empty means clean). / git status --porcelain 摘要 (空字符串即 clean). } // DetectRepo reports whether cwd sits inside a git work tree. // // DetectRepo 检测 cwd 是否位于 git 工作树内. func DetectRepo(cwd string) bool { cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree") cmd.Dir = cwd out, err := cmd.Output() if err != nil { return false } return strings.TrimSpace(string(out)) == "true" } // GetBranch returns the current branch name, or "" when in detached // HEAD / non-repo. Uses `git branch --show-current` — the clean modern // form where detached HEAD produces empty output (easier for callers // than special-casing the literal "HEAD" returned by rev-parse). // // GetBranch 获取当前分支名; detached HEAD 或非仓库返回 "". 采用 // `git branch --show-current` 现代写法, detached HEAD 直接输出空串, // 比 rev-parse 返回字面 "HEAD" 更易消费. func GetBranch(cwd string) string { cmd := exec.Command("git", "branch", "--show-current") cmd.Dir = cwd out, err := cmd.Output() if err != nil { return "" } return strings.TrimSpace(string(out)) } // GetStatus returns git status --porcelain output (empty == clean tree). // // GetStatus 返回 git status --porcelain 输出 (空字符串即 clean). func GetStatus(cwd string) string { cmd := exec.Command("git", "status", "--porcelain") cmd.Dir = cwd out, err := cmd.Output() if err != nil { return "" } return strings.TrimSpace(string(out)) } // GetInfo collects IsRepo / Branch / Status in one call. Used by // pkg/context to fill EnvInfo for system prompt injection. // // GetInfo 一站式收集 IsRepo / Branch / Status 字段, 供 pkg/context // 填充 EnvInfo 注入系统提示词. func GetInfo(cwd string) *Info { info := &Info{} if !DetectRepo(cwd) { return info } info.IsRepo = true info.Branch = GetBranch(cwd) info.Status = GetStatus(cwd) return info } // ValidateRef guards ref args against path traversal and flag injection. // // ELEVATED: exec.Command separates argv (no shell), but a ref starting // with ".." or "-" can still trigger git's own path/option parsing. // Alternative rejected: strict regex whitelist ^[a-zA-Z0-9/_.-]+$ — would // reject valid unicode branch names. // // ValidateRef 拦截 git ref 参数里的路径遍历和选项注入. // // 升华改进 (ELEVATED): exec.Command 按 argv 分离 (不经 shell), // 但 ref 值含 ".." 或以 "-" 起头仍会命中 git 内部的路径 / 选项解析. // 替代方案 (已否决): 正则白名单 ^[a-zA-Z0-9/_.-]+$ — 会误拦合法的 unicode 分支名. func ValidateRef(ref string) error { if strings.Contains(ref, "..") { return fmt.Errorf("git: ref %q contains '..' (path traversal rejected)", ref) } if strings.HasPrefix(ref, "-") { return fmt.Errorf("git: ref %q starts with '-' (flag injection rejected)", ref) } return nil } // GetDiff returns git diff output vs optional ref (empty ref compares working tree to index). // // GetDiff 获取 git diff 输出 (空 ref 时对比工作区与暂存区). func GetDiff(cwd, ref string) string { var cmd *exec.Cmd if ref == "" { cmd = exec.Command("git", "diff") } else { if err := ValidateRef(ref); err != nil { return "" } cmd = exec.Command("git", "diff", ref) } cmd.Dir = cwd out, err := cmd.Output() if err != nil { return "" } return strings.TrimSpace(string(out)) } // GetLog returns the most recent n commits in --oneline format. // n<=0 falls back to 10. // // GetLog 返回最近 n 条提交 (--oneline 格式); n<=0 回退到 10. func GetLog(cwd string, n int) string { if n <= 0 { n = 10 } cmd := exec.Command("git", "log", "--oneline", "-n", strconv.Itoa(n)) cmd.Dir = cwd out, err := cmd.Output() if err != nil { return "" } return strings.TrimSpace(string(out)) }