feat(commit): add commit inspection shortcuts
This commit is contained in:
parent
71ca2bb683
commit
0e149b3cbe
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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 预检。
|
||||
Loading…
Reference in New Issue