# gitcmd - 统一 Git 命令执行层 `gitcmd` 是 simplegit 的底层命令执行器,所有 git shell 调用必须经过这里。 ## 设计原则 1. **安全优先**:禁止用户输入以 `-` 开头,防止选项注入 2. **上下文贯穿**:所有方法接受 `context.Context`,支持取消和超时 3. **环境隔离**:`GIT_CONFIG_NOSYSTEM=1`,不受系统全局配置影响 4. **统一入口**:所有 git 命令走同一条代码路径,便于调试和审计 ## Repository API 速查表 所有方法都绑定在 `Repository` 结构体上,开箱即用,自动处理安全校验和路径管理: | 分类 | 方法 | 功能说明 | |------|------|----------| | **基础操作** | `OpenRepository(ctx, path)` | 打开已存在的Git仓库 | | | `repo.IsEmpty()` | 检查仓库是否为空(无任何提交) | | | `repo.DefaultBranch()` | 获取默认分支名 | | | `repo.WithContext(ctx)` | 切换上下文(用于超时/取消控制) | | | `repo.Command(args...)` | 创建绑定当前仓库的自定义Git命令 | | **提交操作** | `repo.GetCommit(ref)` | 获取单个提交的基础信息 | | | `repo.ListCommits(ref, limit)` | 批量列出提交记录,limit<=0表示全量 | | | `repo.GetCommitDetail(sha)` | 获取提交的完整变更统计(文件列表、行数增减) | | **分支/标签** | `repo.ListBranches()` | 列出所有分支及分支最新提交信息 | | | `repo.ListTags()` | 列出所有标签及标签信息 | | **文件/目录** | `repo.GetBlob(ref, path)` | 获取文件内容及元数据,自动检测二进制文件 | | | `repo.GetRawContent(ref, path)` | 直接获取文件原始内容,无额外元数据,适合下载 | | | `repo.GetTree(ref, path)` | 获取目录下的文件/子目录列表 | | **统计分析** | `repo.GetStats()` | 获取仓库整体统计(总提交数、分支数、标签数、贡献者等) | | | `repo.GetContributors(ref)` | 获取贡献者列表及贡献统计(提交数、代码行数) | | **全局工具** | `InitBare(ctx, path)` | 初始化新的裸仓库 | | | `CloneBare(ctx, src, dest)` | 克隆远程/本地仓库为裸仓库 | --- ## API 示例 ### 基础 Command 使用 ```go import "simplegit/gitcmd" // 基础命令(参数是 TRUSTED 的常量) out, _, err := gitcmd.NewCommand("version").RunStdString(ctx) // 带选项(仍然是 TRUSTED) gitcmd.NewCommand("log", "-1", "--pretty=%H"). WithDir(repoPath). WithTimeout(10*time.Second). RunStdString(ctx) // 用户提供的值必须走 AddDynamicArguments // → 如果 userName 是 "-oCommand=evil",会被直接拒绝 gitcmd.NewCommand("log", "--author"). AddDynamicArguments(userName). WithDir(repoPath). RunStdString(ctx) // 文件列表用 AddDashesAndList(自动加 -- 分隔) gitcmd.NewCommand("ls-tree", "HEAD"). AddDashesAndList(files...). WithDir(repoPath). RunStdString(ctx) ``` ### Repository API 使用(推荐,更通用更安全) `Repository` 封装了单个 Git 仓库的所有操作,自动处理路径、上下文和安全参数验证,不需要每次手动调用 `WithDir`。 #### 1. 打开/创建仓库 ```go // 打开已存在的仓库 repo, err := gitcmd.OpenRepository(ctx, "/path/to/repo.git") if err != nil { // 处理错误:路径不存在、不是有效Git仓库等 log.Fatal(err) } // 初始化新的裸仓库 err := gitcmd.InitBare(ctx, "/path/to/new/repo.git") // 克隆裸仓库 err := gitcmd.CloneBare(ctx, "https://github.com/user/repo.git", "/path/to/clone.git") ``` #### 2. 基础仓库信息 ```go // 检查仓库是否是空的(无提交) isEmpty, err := repo.IsEmpty() // 获取默认分支名 defaultBranch, err := repo.DefaultBranch() // 切换上下文(例如用于请求超时取消) newCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() repoWithTimeout := repo.WithContext(newCtx) ``` #### 3. 提交操作 ```go // 获取单个提交(支持 SHA、分支名、标签名) commit, err := repo.GetCommit("main") fmt.Printf("最新提交: %s %s\n", commit.ShortSHA, commit.Message) // 列出最近20条提交 commits, err := repo.ListCommits("main", 20) // 获取提交详情(包含文件变更统计) detail, err := repo.GetCommitDetail(commit.ID) fmt.Printf("变更文件数: %d, 新增行: %d, 删除行: %d\n", detail.Stats.Files, detail.Stats.Additions, detail.Stats.Deletions) ``` #### 4. 分支和标签 ```go // 列出所有分支 branches, err := repo.ListBranches() for _, b := range branches { fmt.Printf("%s %s %s\n", b.Name, b.ShortSHA, b.Message) } // 列出所有标签 tags, err := repo.ListTags() for _, t := range tags { fmt.Printf("%s %s %s\n", t.Name, t.ShortSHA, t.Message) } ``` #### 5. 文件和目录操作 ```go // 获取文件内容 blob, err := repo.GetBlob("main", "README.md") if !blob.IsBinary { fmt.Println(blob.Content) } // 直接获取原始内容(无元数据,适合下载) content, err := repo.GetRawContent("main", "path/to/file.go") // 列出目录内容 tree, err := repo.GetTree("main", "path/to/dir") for _, entry := range tree.Entries { fmt.Printf("%s %6d %s\n", entry.Type, entry.Size, entry.Name) } ``` #### 6. 统计信息 ```go // 获取仓库整体统计 stats, err := repo.GetStats() fmt.Printf("总提交数: %d, 分支数: %d, 标签数: %d, 贡献者数: %d\n", stats.TotalCommits, stats.TotalBranches, stats.TotalTags, len(stats.Contributors)) // 获取贡献者列表(按提交数排序) contributors, err := repo.GetContributors("main") for _, c := range contributors { fmt.Printf("%s <%s>: %d commits, %d lines changed\n", c.Author, c.Email, c.Commits, c.TotalLines) } ``` #### 7. 执行自定义命令 如果需要执行 `Repository` 没有封装的 Git 命令,可以使用 `repo.Command()` 方法,它会自动设置工作目录: ```go // 等同于 git cmd.NewCommand("status").WithDir(repo.Path) out, _, err := repo.Command("status", "--porcelain").RunStdString(ctx) // 带用户参数的自定义命令 out, _, err := repo.Command("rev-list", "--count"). AddDynamicArguments(userProvidedRef). RunStdString(ctx) ``` ## 安全规则 | 方法 | 用途 | 安全检查 | |---|---|---| | `NewCommand(args...)` | 创建命令,子命令和固定选项 | 参数必须不包含 `=` 或特殊字符 | | `Add(args...)` | 添加更多选项 | 同上 | | `Addf(format, value)` | 选项+值组合 | format 必须安全,值不做检查 | | `AddDynamicArguments(args...)` | 用户输入 | 禁止以 `-` 开头 | | `AddDashesAndList(list...)` | 文件路径列表 | 加 `--` 前缀,防路径以 `-` 开头注入选项 | ## 典型错误 ❌ **错误**:直接拼接用户输入 ```go // 错!如果 ref 是 "--exec=/bin/sh" 怎么办? gitcmd.NewCommand("show", ref).RunStdString(ctx) ``` ✅ **正确**:动态参数走安全方法 ```go // 对!检测以 - 开头的参数并拒绝 gitcmd.NewCommand("show").AddDynamicArguments(ref).RunStdString(ctx) ``` ✅ 或者用 `--` 分隔: ```go gitcmd.NewCommand("show").AddDashesAndList(ref).RunStdString(ctx) ```