203 lines
6.9 KiB
Markdown
203 lines
6.9 KiB
Markdown
# 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)
|
||
```
|