forked from Gitlink/gitlink-cli
648 lines
19 KiB
Markdown
648 lines
19 KiB
Markdown
# PR Diff 功能实现方案
|
||
|
||
## 一、问题定义
|
||
|
||
### 现状
|
||
|
||
`pr +diff` 和 `pr +files` 实现完全相同——都调用 `/pulls/{index}/files.json`(简版文件列表 API),返回文件名和增删行数统计,**不包含任何差异内容**。
|
||
|
||
```go
|
||
// shortcuts/pr/pr.go — 当前 +diff 实现(与 +files 一模一样)
|
||
{
|
||
Name: "diff",
|
||
Description: "Show diff for a pull request",
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
// ...
|
||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||
// ...
|
||
},
|
||
}
|
||
```
|
||
|
||
### 用户期望
|
||
|
||
`pr +diff` 应输出类似 `git diff` 的内容:每个文件的增删行和具体变更,而不仅仅是文件列表。
|
||
|
||
---
|
||
|
||
## 二、GitLink API 调研
|
||
|
||
经过对 `gitlink_api_reference.md` 的全面分析,GitLink 提供了 **3 个** 与 diff 相关的 API 端点:
|
||
|
||
### 端点 1:PR 文件列表(简版)— 当前 `+files` 使用的
|
||
|
||
```
|
||
GET /api/v1/{owner}/{repo}/pulls/{index}/files.json
|
||
```
|
||
|
||
- **返回内容**:文件名、增删行数统计、SHA(**无 diff 内容**)
|
||
- **适用场景**:文件概览列表,适合 `+files` 命令
|
||
|
||
### 端点 2:PR 版本列表
|
||
|
||
```
|
||
GET /api/v1/{owner}/{repo}/pulls/{index}/versions.json
|
||
```
|
||
|
||
- **返回内容**:PR 的每次推送产生一个 version,包含 `id`、`add_line_num`、`del_line_num`、`commits_count`、`files_count`、各 commit SHA
|
||
- **关键作用**:获取最新 version 的 `id`,用于调用端点 3
|
||
|
||
返回示例:
|
||
|
||
```json
|
||
{
|
||
"total_count": 2,
|
||
"versions": [
|
||
{
|
||
"id": 38,
|
||
"add_line_num": 1,
|
||
"del_line_num": 0,
|
||
"files_count": 1,
|
||
"base_commit_sha": "96dc82d...",
|
||
"head_commit_sha": "37d52b...",
|
||
"start_commit_sha": "96dc82d..."
|
||
},
|
||
{
|
||
"id": 39,
|
||
"add_line_num": 5,
|
||
"del_line_num": 3,
|
||
"files_count": 2,
|
||
...
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 端点 3:PR 版本 Diff(核心端点)— 需要使用
|
||
|
||
```
|
||
GET /api/v1/{owner}/{repo}/pulls/{index}/versions/{version_id}/diff.json
|
||
```
|
||
|
||
- **返回内容**:完整的 diff 信息,包含每个文件的 `sections` → `lines`,每行有 `type`(1=新增/2=修改/3=删除/4=统计头)和 `content`
|
||
- **可选参数**:`filepath` 查询参数可只查看单个文件的 diff
|
||
|
||
返回示例:
|
||
|
||
```json
|
||
{
|
||
"file_nums": 1,
|
||
"total_addition": 1,
|
||
"total_deletion": 0,
|
||
"files": [
|
||
{
|
||
"name": "main.go",
|
||
"oldname": "main.go",
|
||
"addition": 1,
|
||
"deletion": 0,
|
||
"type": 1,
|
||
"is_created": true,
|
||
"sections": [
|
||
{
|
||
"file_name": "main.go",
|
||
"lines": [
|
||
{
|
||
"type": 4,
|
||
"content": "@@ -0,0 +1 @@"
|
||
},
|
||
{
|
||
"type": 2,
|
||
"content": "+fmt.Println(\"hello\")"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 端点 4(备选):Compare Diff
|
||
|
||
```
|
||
GET /api/v1/{owner}/{repo}/compare.json
|
||
GET /api/{owner}/{repo}/compare/{head}...{base}.json
|
||
```
|
||
|
||
- **返回内容**:两个分支间的完整 diff(结构与端点 3 类似)
|
||
- **适用场景**:本地分支比较,不依赖 PR 编号
|
||
|
||
---
|
||
|
||
## 三、实现方案
|
||
|
||
### 方案选择:基于 PR Versions API 的两步调用
|
||
|
||
**调用流程**:
|
||
|
||
```
|
||
用户执行: gitlink-cli pr +diff --id 42
|
||
│
|
||
▼
|
||
Step 1: GET /{owner}/{repo}/pulls/42/versions.json
|
||
│ 获取版本列表,取最新版本的 id
|
||
│
|
||
▼
|
||
Step 2: GET /{owner}/{repo}/pulls/42/versions/{version_id}/diff.json
|
||
│ 获取完整 diff 数据
|
||
│
|
||
▼
|
||
格式化输出(unified diff 风格 / JSON / 统计摘要)
|
||
```
|
||
|
||
### 3.1 核心实现:修改 `shortcuts/pr/pr.go`
|
||
|
||
#### 3.1.1 新增 `+diff` Shortcut
|
||
|
||
替换现有的 `+diff` 命令实现:
|
||
|
||
```go
|
||
{
|
||
Name: "diff",
|
||
Description: "Show diff for a pull request",
|
||
Flags: []common.Flag{
|
||
{Name: "id", Short: "i", Usage: "PR number", Required: true},
|
||
{Name: "file", Short: "f", Usage: "Filter diff to a specific file path"},
|
||
{Name: "stat", Usage: "Show only diff stat summary (no line-level detail)", Bool: true},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
id, _ := ctx.RequireArg("id")
|
||
|
||
// Step 1: 获取最新 version ID
|
||
versionID, err := getLatestVersionID(ctx, id)
|
||
if err != nil {
|
||
return fmt.Errorf("获取 PR 版本失败: %w", err)
|
||
}
|
||
|
||
// Step 2: 获取 diff 数据
|
||
diffPath := fmt.Sprintf("/v1%s/pulls/%s/versions/%s/diff",
|
||
ctx.RepoPath(), id, versionID)
|
||
q := url.Values{}
|
||
if f := ctx.Arg("file"); f != "" {
|
||
q.Set("filepath", f)
|
||
}
|
||
env, err := ctx.CallAPIWithQuery("GET", diffPath, q)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Step 3: 格式化输出
|
||
if ctx.Arg("stat") == "true" {
|
||
return ctx.Output(formatDiffStat(env))
|
||
}
|
||
return ctx.Output(formatDiffUnified(env))
|
||
},
|
||
},
|
||
```
|
||
|
||
#### 3.1.2 辅助函数:获取最新 Version ID
|
||
|
||
```go
|
||
// getLatestVersionID 调用 versions API 并返回最新版本的 ID。
|
||
// GitLink 返回的 versions 数组按时间倒序排列,第一个即最新。
|
||
func getLatestVersionID(ctx *common.RuntimeContext, prID string) (string, error) {
|
||
env, err := ctx.CallAPI("GET",
|
||
fmt.Sprintf("/v1%s/pulls/%s/versions", ctx.RepoPath(), prID), nil)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
data, ok := env.Data.(map[string]interface{})
|
||
if !ok {
|
||
return "", fmt.Errorf("unexpected versions response format")
|
||
}
|
||
|
||
versions, ok := data["versions"].([]interface{})
|
||
if !ok || len(versions) == 0 {
|
||
return "", fmt.Errorf("no versions found for PR #%s", prID)
|
||
}
|
||
|
||
latest, ok := versions[0].(map[string]interface{})
|
||
if !ok {
|
||
return "", fmt.Errorf("unexpected version format")
|
||
}
|
||
|
||
idFloat, ok := latest["id"].(float64)
|
||
if !ok {
|
||
return "", fmt.Errorf("version missing id field")
|
||
}
|
||
|
||
return fmt.Sprintf("%d", int64(idFloat)), nil
|
||
}
|
||
```
|
||
|
||
#### 3.1.3 格式化:统一 Diff 风格输出
|
||
|
||
将 GitLink API 返回的 `sections/lines` 结构转为类似 `git diff` 的文本格式:
|
||
|
||
```go
|
||
// formatDiffUnified 将 API diff 数据转为 unified diff 风格的 Envelope。
|
||
// 在 JSON 模式下直接输出原始结构;在 table 模式下输出 diff 文本。
|
||
func formatDiffUnified(env *output.Envelope) *output.Envelope {
|
||
// 直接返回原始数据,让 formatter 处理
|
||
// JSON/YAML 模式下输出完整结构化数据
|
||
// Table 模式下由自定义渲染器处理
|
||
return env
|
||
}
|
||
|
||
// formatDiffStat 提取增删统计摘要。
|
||
func formatDiffStat(env *output.Envelope) *output.Envelope {
|
||
data, ok := env.Data.(map[string]interface{})
|
||
if !ok {
|
||
return env
|
||
}
|
||
|
||
stat := map[string]interface{}{
|
||
"file_nums": data["file_nums"],
|
||
"total_addition": data["total_addition"],
|
||
"total_deletion": data["total_deletion"],
|
||
}
|
||
|
||
// 提取每个文件的简要统计
|
||
if files, ok := data["files"].([]interface{}); ok {
|
||
var fileStats []map[string]interface{}
|
||
for _, f := range files {
|
||
if fm, ok := f.(map[string]interface{}); ok {
|
||
fileStats = append(fileStats, map[string]interface{}{
|
||
"name": fm["name"],
|
||
"addition": fm["addition"],
|
||
"deletion": fm["deletion"],
|
||
"type": fm["type"],
|
||
})
|
||
}
|
||
}
|
||
stat["files"] = fileStats
|
||
}
|
||
|
||
return output.SuccessEnvelope(stat, nil)
|
||
}
|
||
```
|
||
|
||
### 3.2 增强输出格式化:支持 Diff 文本渲染
|
||
|
||
在 `internal/output/formatter.go` 中新增 diff 格式化能力:
|
||
|
||
```go
|
||
// printDiffTable 渲染 diff 内容为可读的文本格式。
|
||
// 仅当 Data 中包含 diff 结构(有 files[].sections)时生效。
|
||
func printDiffTable(w io.Writer, envelope *Envelope) error {
|
||
data, ok := envelope.Data.(map[string]interface{})
|
||
if !ok {
|
||
return printJSON(w, envelope)
|
||
}
|
||
|
||
files, ok := data["files"].([]interface{})
|
||
if !ok {
|
||
return printJSON(w, envelope)
|
||
}
|
||
|
||
// 统计头部
|
||
fileNums, _ := data["file_nums"].(float64)
|
||
totalAdd, _ := data["total_addition"].(float64)
|
||
totalDel, _ := data["total_deletion"].(float64)
|
||
fmt.Fprintf(w, " %d files changed, %d insertions(+), %d deletions(-)\n\n",
|
||
int(fileNums), int(totalAdd), int(totalDel))
|
||
|
||
for _, f := range files {
|
||
fm, ok := f.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
name, _ := fm["name"].(string)
|
||
addition, _ := fm["addition"].(float64)
|
||
deletion, _ := fm["deletion"].(float64)
|
||
|
||
// 文件头
|
||
fmt.Fprintf(w, "diff --git a/%s b/%s\n", name, name)
|
||
|
||
if isCreated, _ := fm["is_created"].(bool); isCreated {
|
||
fmt.Fprintf(w, "new file\n")
|
||
}
|
||
if isDeleted, _ := fm["is_deleted"].(bool); isDeleted {
|
||
fmt.Fprintf(w, "deleted file\n")
|
||
}
|
||
|
||
fmt.Fprintf(w, "--- a/%s\n", name)
|
||
fmt.Fprintf(w, "+++ b/%s\n", name)
|
||
fmt.Fprintf(w, "@@ +%d -%d @@\n", int(addition), int(deletion))
|
||
|
||
// 渲染每一行
|
||
sections, _ := fm["sections"].([]interface{})
|
||
for _, sec := range sections {
|
||
secMap, ok := sec.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
lines, _ := secMap["lines"].([]interface{})
|
||
for _, l := range lines {
|
||
lineMap, ok := l.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
content, _ := lineMap["content"].(string)
|
||
lineType, _ := lineMap["type"].(float64)
|
||
|
||
switch int(lineType) {
|
||
case 4: // diff hunk header
|
||
fmt.Fprintf(w, "%s\n", content)
|
||
case 2: // addition
|
||
fmt.Fprintf(w, "\033[32m%s\033[0m\n", content)
|
||
case 3: // deletion
|
||
fmt.Fprintf(w, "\033[31m%s\033[0m\n", content)
|
||
default: // context line
|
||
fmt.Fprintf(w, "%s\n", content)
|
||
}
|
||
}
|
||
}
|
||
fmt.Fprintln(w)
|
||
}
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 3.3 修改 `printTable` 路由以支持 diff 格式
|
||
|
||
在 `internal/output/formatter.go` 的 `printTable` 函数中增加 diff 检测逻辑:
|
||
|
||
```go
|
||
func printTable(w io.Writer, envelope *Envelope) error {
|
||
// ... 现有错误和空数据处理 ...
|
||
|
||
// 检测是否为 diff 数据(包含 files[].sections)
|
||
if isDiffData(envelope.Data) {
|
||
return printDiffTable(w, envelope)
|
||
}
|
||
|
||
// ... 现有 slice/map 处理逻辑 ...
|
||
}
|
||
|
||
// isDiffData 检测 Envelope.Data 是否为 PR diff 结构。
|
||
func isDiffData(data interface{}) bool {
|
||
m, ok := data.(map[string]interface{})
|
||
if !ok {
|
||
return false
|
||
}
|
||
// diff 数据的特征:有 file_nums 和 files 字段
|
||
_, hasFileNums := m["file_nums"]
|
||
_, hasFiles := m["files"]
|
||
_, hasTotalAdd := m["total_addition"]
|
||
return hasFileNums && hasFiles && hasTotalAdd
|
||
}
|
||
```
|
||
|
||
### 3.4 完整的文件变更清单
|
||
|
||
| 文件 | 变更类型 | 说明 |
|
||
|------|----------|------|
|
||
| `shortcuts/pr/pr.go` | **修改** | 替换 `+diff` 实现,新增 `getLatestVersionID`、`formatDiffUnified`、`formatDiffStat` 函数 |
|
||
| `internal/output/formatter.go` | **修改** | 新增 `isDiffData`、`printDiffTable` 函数,修改 `printTable` 路由 |
|
||
| `shortcuts/pr/pr_test.go` | **修改** | 新增 `+diff` 功能测试用例 |
|
||
| `skills/gitlink-pr/SKILL.md` | **修改** | 更新 `+diff` 命令文档,说明新增的 `--file` 和 `--stat` 参数 |
|
||
| `skills/gitlink-pr/references/gitlink-pr-files.md` | **修改** | 补充 diff 与 files 的区别说明 |
|
||
|
||
---
|
||
|
||
## 四、测试方案
|
||
|
||
### 4.1 单元测试
|
||
|
||
在 `shortcuts/pr/pr_test.go` 中新增以下测试用例:
|
||
|
||
| 测试用例 | 验证内容 |
|
||
|----------|----------|
|
||
| `TestPRDiffFetchesVersionThenDiff` | 完整两步调用:先请求 versions,再请求 diff,验证最终输出正确 |
|
||
| `TestPRDiffWithFileFilter` | 验证 `--file` 参数正确传递 `filepath` 查询参数 |
|
||
| `TestPRDiffStatMode` | 验证 `--stat` 模式只输出统计摘要,不输出逐行内容 |
|
||
| `TestPRDiffFailsWhenNoVersions` | PR 无版本时返回友好错误信息 |
|
||
| `TestPRDiffFailsWhenPRNotFound` | PR 不存在时(404)正确处理错误 |
|
||
| `TestGetLatestVersionID` | 直接测试 `getLatestVersionID` 函数,验证取第一个版本 |
|
||
| `TestFormatDiffStat` | 测试统计摘要格式化逻辑 |
|
||
|
||
测试 Mock Server 示例:
|
||
|
||
```go
|
||
func TestPRDiffFetchesVersionThenDiff(t *testing.T) {
|
||
var requestPaths []string
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
requestPaths = append(requestPaths, r.URL.Path)
|
||
|
||
switch {
|
||
// Step 1: versions 请求
|
||
case strings.Contains(r.URL.Path, "/versions.json") &&
|
||
!strings.Contains(r.URL.Path, "/diff"):
|
||
writeJSON(t, w, map[string]interface{}{
|
||
"total_count": 1,
|
||
"versions": []interface{}{
|
||
map[string]interface{}{
|
||
"id": float64(38),
|
||
"add_line_num": 3,
|
||
"del_line_num": 1,
|
||
"files_count": 2,
|
||
},
|
||
},
|
||
})
|
||
|
||
// Step 2: diff 请求
|
||
case strings.Contains(r.URL.Path, "/versions/38/diff"):
|
||
writeJSON(t, w, map[string]interface{}{
|
||
"file_nums": float64(2),
|
||
"total_addition": float64(3),
|
||
"total_deletion": float64(1),
|
||
"files": []interface{}{
|
||
map[string]interface{}{
|
||
"name": "main.go",
|
||
"addition": float64(2),
|
||
"deletion": float64(1),
|
||
"sections": []interface{}{
|
||
map[string]interface{}{
|
||
"lines": []interface{}{
|
||
map[string]interface{}{
|
||
"type": float64(4),
|
||
"content": "@@ -10,3 +10,4 @@",
|
||
},
|
||
map[string]interface{}{
|
||
"type": float64(3),
|
||
"content": "-old line",
|
||
},
|
||
map[string]interface{}{
|
||
"type": float64(2),
|
||
"content": "+new line",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
}
|
||
}))
|
||
defer server.Close()
|
||
|
||
err := runPRShortcut(t, server, "diff", map[string]string{
|
||
"id": "42",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("diff shortcut failed: %v", err)
|
||
}
|
||
|
||
// 验证两次请求都发生了
|
||
if len(requestPaths) < 2 {
|
||
t.Fatalf("expected 2 API calls, got %d: %v", len(requestPaths), requestPaths)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.2 集成测试建议
|
||
|
||
由于需要真实 GitLink API,建议:
|
||
|
||
1. 在测试仓库上创建测试 PR(含多文件变更)
|
||
2. 验证 `gitlink-cli pr +diff --id <N>` 输出与 Web UI 显示一致
|
||
3. 验证 `--file` 过滤功能
|
||
4. 验证 `--stat` 模式只输出统计
|
||
|
||
---
|
||
|
||
## 五、API 路径注意事项
|
||
|
||
当前项目 Client 的 `.json` 后缀自动追加逻辑([client.go:44-55](gitlink-cli/internal/client/client.go#L44-L55))会自动为路径添加 `.json`,因此代码中写路径时**不需要手动加 `.json`**:
|
||
|
||
```go
|
||
// 正确 — Client 会自动追加 .json
|
||
"/v1%s/pulls/%s/versions"
|
||
"/v1%s/pulls/%s/versions/%s/diff"
|
||
|
||
// 错误 — 会导致双重后缀
|
||
"/v1%s/pulls/%s/versions.json" // → versions.json.json
|
||
"/v1%s/pulls/%s/versions/%s/diff.json" // → diff.json.json
|
||
```
|
||
|
||
注意:现有 `+files` 使用的是无 `/v1` 前缀的路径 `/{owner}/{repo}/pulls/{index}/files`,而 versions 端点的文档路径为 `/api/v1/{owner}/{repo}/pulls/{index}/versions.json`。需要验证 Client 的 BaseURL 是否已包含 `/api` 前缀。查看 config 默认值:
|
||
|
||
```go
|
||
// internal/config/config.go 中 BaseURL 默认值
|
||
// 需要确认是否为 "https://www.gitlink.org.cn/api"
|
||
// 如果是,则路径写为 "/v1/{owner}/{repo}/pulls/{index}/versions"
|
||
```
|
||
|
||
---
|
||
|
||
## 六、风险与降级策略
|
||
|
||
| 风险 | 影响 | 降级方案 |
|
||
|------|------|----------|
|
||
| GitLink versions API 不稳定或返回空 | 无法获取 diff | 保留旧 `+files` 行为作为 fallback,输出提示 "diff unavailable, showing file list only" |
|
||
| 大型 PR 的 diff 数据量过大 | 响应慢、终端刷屏 | 默认只显示统计摘要(`--stat`),加 `--full` 参数才显示逐行内容 |
|
||
| API 路径前缀与实际不匹配 | 请求 404 | 开发时先通过 `api` 命令验证路径:`gitlink-cli api GET /v1/owner/repo/pulls/1/versions` |
|
||
| table 模式下 diff 文本格式化复杂 | 渲染异常 | table 模式 fallback 到 JSON 输出 |
|
||
|
||
---
|
||
|
||
## 七、实现步骤(Checklist)
|
||
|
||
- [ ] **Step 1**:验证 API 端点可达性
|
||
```bash
|
||
gitlink-cli api GET /v1/{owner}/{repo}/pulls/1/versions
|
||
gitlink-cli api GET /v1/{owner}/{repo}/pulls/1/versions/{vid}/diff
|
||
```
|
||
确认 BaseURL + 路径组合正确
|
||
|
||
- [ ] **Step 2**:修改 `shortcuts/pr/pr.go`
|
||
- 替换 `+diff` 的 `Run` 函数为两步调用逻辑
|
||
- 新增 `getLatestVersionID` 函数
|
||
- 新增 `formatDiffUnified`、`formatDiffStat` 函数
|
||
- 为 `+diff` 添加 `--file` 和 `--stat` 参数
|
||
|
||
- [ ] **Step 3**:修改 `internal/output/formatter.go`
|
||
- 新增 `isDiffData` 检测函数
|
||
- 新增 `printDiffTable` 渲染函数
|
||
- 修改 `printTable` 路由增加 diff 分支
|
||
|
||
- [ ] **Step 4**:编写测试
|
||
- `shortcuts/pr/pr_test.go` 新增 5-7 个测试用例
|
||
- 使用 httptest.Server mock versions 和 diff 端点
|
||
|
||
- [ ] **Step 5**:更新 Skill 文档
|
||
- 更新 `skills/gitlink-pr/SKILL.md`
|
||
- 更新 `skills/gitlink-pr/references/gitlink-pr-files.md`
|
||
|
||
- [ ] **Step 6**:手动集成测试
|
||
- 在真实 GitLink 仓库上验证完整流程
|
||
|
||
---
|
||
|
||
## 八、预期效果
|
||
|
||
### 命令使用示例
|
||
|
||
```bash
|
||
# 查看完整 diff(默认)
|
||
gitlink-cli pr +diff --id 42
|
||
|
||
# 只查看某个文件的 diff
|
||
gitlink-cli pr +diff --id 42 --file "main.go"
|
||
|
||
# 只看统计摘要(适合大型 PR)
|
||
gitlink-cli pr +diff --id 42 --stat
|
||
|
||
# JSON 格式输出(适合脚本处理)
|
||
gitlink-cli pr +diff --id 42 --format json
|
||
```
|
||
|
||
### 输出示例(table 模式)
|
||
|
||
```
|
||
2 files changed, 15 insertions(+), 3 deletions(-)
|
||
|
||
diff --git a/main.go b/main.go
|
||
new file
|
||
--- a/main.go
|
||
+++ b/main.go
|
||
@@ +5 -2 @@
|
||
@@ -10,3 +10,4 @@
|
||
-old implementation
|
||
+new implementation
|
||
+another new line
|
||
|
||
diff --git a/README.md b/README.md
|
||
--- a/README.md
|
||
+++ b/README.md
|
||
@@ +3 -1 @@
|
||
@@ -5,7 +5,6 @@
|
||
-removed section
|
||
```
|
||
|
||
### 输出示例(--stat 模式)
|
||
|
||
```
|
||
FILE ADDITION DELETION TYPE
|
||
--- -------- -------- ----
|
||
main.go 15 2 added
|
||
README.md 0 1 modified
|
||
```
|
||
|
||
### 输出示例(JSON 模式)
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"data": {
|
||
"file_nums": 2,
|
||
"total_addition": 15,
|
||
"total_deletion": 3,
|
||
"files": [
|
||
{
|
||
"name": "main.go",
|
||
"addition": 15,
|
||
"deletion": 2,
|
||
"sections": [...]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|