From 88df73b92d41581b8c39e7c0ee5379542d7c1398 Mon Sep 17 00:00:00 2001 From: 1os21ka23r9navae6mrro <1os21ka23r9navae6mrro@gmail.com> Date: Wed, 8 Jul 2026 14:06:45 +0000 Subject: [PATCH] feat(repo): add +blame for line-by-line file attribution Wraps GET /api/v1/:owner/:repo/blame (params: filepath, sha) as repo +blame -p [--ref ], defaulting ref to master. Production-verified on gitlink.org.cn (875-line file returned 274 blame parts with commit/author metadata). 2 unit tests, README example, and bilingual i18n keys. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 3 +++ internal/i18n/locales/en-US.json | 2 ++ internal/i18n/locales/zh-CN.json | 2 ++ shortcuts/repo/repo.go | 29 ++++++++++++++++++++++++++++ shortcuts/repo/repo_test.go | 33 ++++++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+) diff --git a/README.md b/README.md index e5e4318..d1997a5 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,9 @@ gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main +# Line-by-line blame for a file +gitlink-cli repo +blame --owner Gitlink --repo forgeplus -p README.md --ref master + # Show language breakdown gitlink-cli repo +languages --owner Gitlink --repo forgeplus diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..9608373 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -81,6 +81,7 @@ "cmd.release.list.short": "List releases", "cmd.release.short": "Release operations", "cmd.release.view.short": "View release details", + "cmd.repo.blame.short": "Show line-by-line blame for a file", "cmd.repo.create.short": "Create a new repository", "cmd.repo.delete.short": "Delete a repository", "cmd.repo.fork.short": "Fork a repository", @@ -208,6 +209,7 @@ "flag.release.tag": "Tag name", "flag.release.target": "Target branch", "flag.repo": "Repository name (auto-detected from git remote)", + "flag.repo.blame.path": "File path to blame", "flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)", "flag.repo.description": "Repository description", "flag.repo.name": "Repository name", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..5293b72 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -81,6 +81,7 @@ "cmd.release.list.short": "列出发布", "cmd.release.short": "发布操作", "cmd.release.view.short": "查看发布详情", + "cmd.repo.blame.short": "逐行显示文件的 blame 信息", "cmd.repo.create.short": "创建新仓库", "cmd.repo.delete.short": "删除仓库", "cmd.repo.fork.short": "Fork 仓库", @@ -208,6 +209,7 @@ "flag.release.tag": "标签名称", "flag.release.target": "目标分支", "flag.repo": "仓库名称(自动从 git remote 检测)", + "flag.repo.blame.path": "要 blame 的文件路径", "flag.repo.category": "筛选:manage/mirror/sync/fork/all(默认:manage)", "flag.repo.description": "仓库描述", "flag.repo.name": "仓库名称", diff --git a/shortcuts/repo/repo.go b/shortcuts/repo/repo.go index 06774a6..769a380 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -108,6 +108,35 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "blame", + Description: tr.T("cmd.repo.blame.short"), + Flags: []common.Flag{ + {Name: "path", Short: "p", Usage: tr.T("flag.repo.blame.path"), Required: true}, + {Name: "ref", Short: "r", Usage: tr.T("flag.repo.tree.ref"), Default: "master"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + path, err := ctx.RequireArg("path") + if err != nil { + return err + } + ref := ctx.Arg("ref") + if ref == "" { + ref = "master" + } + q := url.Values{} + q.Set("filepath", path) + q.Set("sha", ref) + env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/blame", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, { Name: "languages", Description: "Show repository language statistics", diff --git a/shortcuts/repo/repo_test.go b/shortcuts/repo/repo_test.go index 5c5f34b..c8eac94 100644 --- a/shortcuts/repo/repo_test.go +++ b/shortcuts/repo/repo_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "github.com/gitlink-org/gitlink-cli/internal/client" @@ -634,3 +635,35 @@ func assertEqual(t *testing.T, got interface{}, want interface{}) { t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want) } } + +func TestRepoBlameUsesBlameEndpoint(t *testing.T) { + var query string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/blame.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + query = r.URL.RawQuery + writeJSON(t, w, map[string]interface{}{"file_name": "README.md", "blame_parts": []interface{}{}}) + })) + defer server.Close() + + err := runShortcut(t, server, "blame", map[string]string{"path": "README.md", "ref": "develop"}) + if err != nil { + t.Fatalf("blame failed: %v", err) + } + if !strings.Contains(query, "filepath=README.md") || !strings.Contains(query, "sha=develop") { + t.Fatalf("unexpected query: %q", query) + } +} + +func TestRepoBlameRequiresPath(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runShortcut(t, server, "blame", map[string]string{}) + if err == nil { + t.Fatal("expected error when --path missing") + } +}