feat(commit): add commit inspection shortcuts

This commit is contained in:
wangyue789 2026-06-10 21:04:24 +08:00
parent 71ca2bb683
commit 0e149b3cbe
6 changed files with 281 additions and 1 deletions

View File

@ -0,0 +1,18 @@
# Commit Inspect Shortcuts
Added `gitlink-cli commit` for read-only commit inspection.
| Command | Endpoint |
| --- | --- |
| `commit +list` | `GET /v1/{owner}/{repo}/commits` |
| `commit +files` | `GET /v1/{owner}/{repo}/commits/{sha}/files` |
| `commit +diff` | `GET /v1/{owner}/{repo}/commits/{sha}/diff` |
| `commit +blame` | `GET /v1/{owner}/{repo}/blame` |
Validation:
```bash
GOPROXY=https://goproxy.cn,direct go test ./shortcuts/commit ./shortcuts
go vet ./shortcuts/commit ./shortcuts
go run . commit --help
```

125
shortcuts/commit/commit.go Normal file
View File

@ -0,0 +1,125 @@
package commit
import (
"fmt"
"net/url"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// Shortcuts returns repository commit inspection shortcuts.
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
{
Name: "list",
Description: "List repository commits",
Flags: []common.Flag{
{Name: "sha", Short: "s", Usage: "Branch, tag, or commit SHA"},
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
setQuery(q, "sha", ctx.Arg("sha"))
setQuery(q, "page", ctx.Arg("page"))
setQuery(q, "limit", ctx.Arg("limit"))
env, err := ctx.CallAPIWithQuery("GET", commitRepoPath(ctx)+"/commits", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "files",
Description: "List changed files for a commit",
Flags: []common.Flag{
{Name: "sha", Short: "s", Usage: "Commit SHA", Required: true},
{Name: "filepath", Short: "f", Usage: "Filter by file path"},
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
sha, err := ctx.RequireArg("sha")
if err != nil {
return err
}
q := url.Values{}
setQuery(q, "filepath", ctx.Arg("filepath"))
setQuery(q, "page", ctx.Arg("page"))
setQuery(q, "limit", ctx.Arg("limit"))
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/commits/%s/files", commitRepoPath(ctx), url.PathEscape(sha)), q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "diff",
Description: "Show diff for a commit",
Flags: []common.Flag{
{Name: "sha", Short: "s", Usage: "Commit SHA", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
sha, err := ctx.RequireArg("sha")
if err != nil {
return err
}
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/commits/%s/diff", commitRepoPath(ctx), url.PathEscape(sha)), nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "blame",
Description: "Show blame information for a file",
Flags: []common.Flag{
{Name: "sha", Short: "s", Usage: "Branch, tag, or commit SHA", Required: true},
{Name: "filepath", Short: "f", Usage: "File path", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
sha, err := ctx.RequireArg("sha")
if err != nil {
return err
}
filepath, err := ctx.RequireArg("filepath")
if err != nil {
return err
}
q := url.Values{}
q.Set("sha", sha)
q.Set("filepath", filepath)
env, err := ctx.CallAPIWithQuery("GET", commitRepoPath(ctx)+"/blame", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
}
}
func commitRepoPath(ctx *common.RuntimeContext) string {
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
}
func setQuery(q url.Values, key, value string) {
if value != "" {
q.Set(key, value)
}
}

View File

@ -0,0 +1,101 @@
package commit
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestCommitList(t *testing.T) {
server := newCommitServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/commits.json")
assertEqual(t, r.URL.Query().Get("sha"), "master")
assertEqual(t, r.URL.Query().Get("page"), "2")
writeJSON(t, w, map[string]interface{}{"total_count": 0, "commits": []interface{}{}})
})
defer server.Close()
if err := runCommitShortcut(t, server, "list", map[string]string{"sha": "master", "page": "2", "limit": "20"}); err != nil {
t.Fatalf("list failed: %v", err)
}
}
func TestCommitFiles(t *testing.T) {
server := newCommitServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/commits/abc/files.json")
assertEqual(t, r.URL.Query().Get("filepath"), "README.md")
writeJSON(t, w, map[string]interface{}{"files": []interface{}{}})
})
defer server.Close()
if err := runCommitShortcut(t, server, "files", map[string]string{"sha": "abc", "filepath": "README.md"}); err != nil {
t.Fatalf("files failed: %v", err)
}
}
func TestCommitDiff(t *testing.T) {
server := newCommitServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/commits/abc/diff.json")
writeJSON(t, w, map[string]interface{}{"files": []interface{}{}})
})
defer server.Close()
if err := runCommitShortcut(t, server, "diff", map[string]string{"sha": "abc"}); err != nil {
t.Fatalf("diff failed: %v", err)
}
}
func TestCommitBlame(t *testing.T) {
server := newCommitServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/blame.json")
assertEqual(t, r.URL.Query().Get("sha"), "master")
assertEqual(t, r.URL.Query().Get("filepath"), "README.md")
writeJSON(t, w, map[string]interface{}{"file_name": "README.md"})
})
defer server.Close()
if err := runCommitShortcut(t, server, "blame", map[string]string{"sha": "master", "filepath": "README.md"}); err != nil {
t.Fatalf("blame failed: %v", err)
}
}
func runCommitShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
var shortcut *common.Shortcut
for _, s := range Shortcuts() {
if s.Name == name {
shortcut = s
}
}
if shortcut == nil {
t.Fatalf("shortcut %q not found", name)
}
return shortcut.Run(&common.RuntimeContext{Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, Owner: "owner", Repo: "repo", Format: "json", Args: args})
}
func newCommitServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
return httptest.NewServer(handler)
}
func assertRequest(t *testing.T, r *http.Request, method, path string) {
t.Helper()
if r.Method != method || r.URL.Path != path {
t.Fatalf("got %s %s, want %s %s", r.Method, r.URL.Path, method, path)
}
}
func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(payload); err != nil {
t.Fatalf("write json: %v", err)
}
}
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts/branch"
"github.com/gitlink-org/gitlink-cli/shortcuts/ci"
"github.com/gitlink-org/gitlink-cli/shortcuts/commit"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
"github.com/gitlink-org/gitlink-cli/shortcuts/compare"
"github.com/gitlink-org/gitlink-cli/shortcuts/dataset"
@ -37,6 +38,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
}
groups := map[string][]*common.Shortcut{
"repo": repo.Shortcuts(tr),
"commit": commit.Shortcuts(),
"issue": issue.Shortcuts(tr),
"label": label.Shortcuts(),
"license": license.Shortcuts(),
@ -62,6 +64,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
descriptions := map[string]string{
"repo": tr.T("cmd.repo.short"),
"commit": "Commit inspection operations",
"issue": tr.T("cmd.issue.short"),
"label": "Issue label operations",
"license": "License operations",

View File

@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
"repo", "issue", "label", "license", "pr", "profile", "release", "branch",
"org", "user", "search", "ci", "workflow",
"compare", "member", "milestone", "pipeline", "webhook",
"dataset", "health", "ignore", "wiki",
"dataset", "health", "ignore", "wiki", "commit",
}
groupSet := map[string]bool{}

View File

@ -0,0 +1,33 @@
---
name: gitlink-commit
version: 1.0.0
description: "GitLink 提交审查提交列表、单个提交文件、diff 与 blame 查询。"
metadata:
requires:
bins: ["gitlink-cli"]
cliHelp: "gitlink-cli commit --help"
---
# gitlink-commit
**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)。**
## Shortcuts
| Shortcut | 说明 |
| --- | --- |
| `commit +list` | 查询提交列表 |
| `commit +files` | 查询单个提交变更文件 |
| `commit +diff` | 查询单个提交 diff |
| `commit +blame` | 查询文件 blame |
## 示例
```bash
gitlink-cli commit +list --owner Gitlink --repo forgeplus --sha master --page 1 --limit 20
gitlink-cli commit +files --owner Gitlink --repo forgeplus --sha <sha> --filepath README.md
gitlink-cli commit +diff --owner Gitlink --repo forgeplus --sha <sha>
gitlink-cli commit +blame --owner Gitlink --repo forgeplus --sha master --filepath README.md
```
全部命令均为只读,适合代码审查、变更追踪、科研仓库复现和 Agent 预检。