package git import ( "errors" "os/exec" "path/filepath" "strings" ) var ErrPathTraversal = errors.New("repo path outside of configured root") var repoRoot string func SetRepoRoot(root string) { repoRoot = filepath.Clean(root) } // run executes a git command inside repoPath with strict safety guarantees: // - repoPath is validated to be under the configured repoRoot // - args are passed as discrete values — never via shell interpolation // - the process inherits only a minimal environment func run(repoPath string, args ...string) ([]byte, error) { clean := filepath.Clean(repoPath) if repoRoot != "" && !strings.HasPrefix(clean, repoRoot+string(filepath.Separator)) && clean != repoRoot { return nil, ErrPathTraversal } cmd := exec.Command("git", args...) cmd.Dir = clean cmd.Env = []string{ "GIT_TERMINAL_PROMPT=0", "HOME=" + filepath.Dir(repoRoot), // needed for .gitconfig lookups } return cmd.Output() } func Init(path string) error { _, err := run(path, "init", "--bare") return err } type Commit struct { Hash string Author string Message string Date string } func Log(repoPath, branch string, limit int) ([]Commit, error) { out, err := run(repoPath, "log", branch, "--format=%H\x1f%an\x1f%s\x1f%ci", "--max-count", strings.TrimSpace(string(rune(limit+'0'))), ) if err != nil { return nil, err } var commits []Commit for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { parts := strings.Split(line, "\x1f") if len(parts) != 4 { continue } commits = append(commits, Commit{ Hash: parts[0], Author: parts[1], Message: parts[2], Date: parts[3], }) } return commits, nil } type TreeEntry struct { Mode string Type string Hash string Name string } func TreeLS(repoPath, ref, subPath string) ([]TreeEntry, error) { treeRef := ref + ":" + subPath out, err := run(repoPath, "ls-tree", treeRef) if err != nil { return nil, err } var entries []TreeEntry for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { if line == "" { continue } // format: SP SP TAB tabIdx := strings.Index(line, "\t") if tabIdx < 0 { continue } name := line[tabIdx+1:] fields := strings.Fields(line[:tabIdx]) if len(fields) != 3 { continue } entries = append(entries, TreeEntry{ Mode: fields[0], Type: fields[1], Hash: fields[2], Name: name, }) } return entries, nil } func BlobCat(repoPath, ref, filePath string) ([]byte, error) { return run(repoPath, "show", ref+":"+filePath) }