feat: add repo languages/contributors/files/tags/commits shortcuts with tests

Add 5 new repo shortcuts:
- languages: show language breakdown
- contributors: list contributors with pagination
- files: list directory contents with ref/path filters
- tags: list tags with pagination
- commits: list commits with sha/path filters

Include 13 unit tests covering normal paths and HTTP error paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
蒋天翔 2026-06-02 17:18:23 +08:00
parent 9ea0dc71b9
commit 9106be35d4
2 changed files with 354 additions and 0 deletions

View File

@ -147,5 +147,115 @@ func Shortcuts() []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "languages",
Description: "Show language breakdown of a repository",
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/languages", nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "contributors",
Description: "List contributors of a repository",
Flags: []common.Flag{
{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{}
q.Set("page", ctx.Arg("page"))
q.Set("limit", ctx.Arg("limit"))
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/contributors", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "files",
Description: "List files in a repository directory",
Flags: []common.Flag{
{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
{Name: "path", Short: "p", Usage: "Directory path (default: repository root)"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
if ref := ctx.Arg("ref"); ref != "" {
q.Set("ref", ref)
}
if p := ctx.Arg("path"); p != "" {
q.Set("filepath", p)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "tags",
Description: "List tags of a repository",
Flags: []common.Flag{
{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{}
q.Set("page", ctx.Arg("page"))
q.Set("limit", ctx.Arg("limit"))
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/tags", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "commits",
Description: "List commits of a repository",
Flags: []common.Flag{
{Name: "sha", Short: "s", Usage: "Branch name, tag, or commit SHA"},
{Name: "path", Short: "p", Usage: "Filter commits 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
}
q := url.Values{}
q.Set("page", ctx.Arg("page"))
q.Set("limit", ctx.Arg("limit"))
if sha := ctx.Arg("sha"); sha != "" {
q.Set("sha", sha)
}
if p := ctx.Arg("path"); p != "" {
q.Set("path", p)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/commits", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
}
}

View File

@ -300,3 +300,247 @@ func TestRepoCreateUserNoLogin(t *testing.T) {
t.Fatal("expected error when user response has no login")
}
}
// --- languages ---
func TestRepoLanguages(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Fatalf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/owner/repo/languages.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(w, map[string]interface{}{
"Go": float64(85.5),
"Shell": float64(14.5),
})
}))
defer server.Close()
err := runShortcut(t, server, "languages", nil)
if err != nil {
t.Fatalf("languages failed: %v", err)
}
}
func TestRepoLanguagesHTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server error"))
}))
defer server.Close()
err := runShortcut(t, server, "languages", nil)
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
// --- contributors ---
func TestRepoContributors(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Fatalf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/owner/repo/contributors.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("page") != "1" {
t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page"))
}
if r.URL.Query().Get("limit") != "20" {
t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit"))
}
writeJSON(w, map[string]interface{}{
"total_count": float64(1),
"data": []interface{}{map[string]interface{}{"login": "alice", "contributions": float64(42)}},
})
}))
defer server.Close()
err := runShortcut(t, server, "contributors", map[string]string{"page": "1", "limit": "20"})
if err != nil {
t.Fatalf("contributors failed: %v", err)
}
}
func TestRepoContributorsHTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server error"))
}))
defer server.Close()
err := runShortcut(t, server, "contributors", map[string]string{"page": "1", "limit": "20"})
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
// --- files ---
func TestRepoFiles(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Fatalf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/owner/repo/files.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(w, []interface{}{
map[string]interface{}{"name": "README.md", "type": "file"},
map[string]interface{}{"name": "src", "type": "dir"},
})
}))
defer server.Close()
err := runShortcut(t, server, "files", nil)
if err != nil {
t.Fatalf("files failed: %v", err)
}
}
func TestRepoFilesWithRef(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("ref") != "main" {
t.Fatalf("expected ref=main, got %s", r.URL.Query().Get("ref"))
}
if r.URL.Query().Get("filepath") != "src" {
t.Fatalf("expected filepath=src, got %s", r.URL.Query().Get("filepath"))
}
writeJSON(w, []interface{}{})
}))
defer server.Close()
err := runShortcut(t, server, "files", map[string]string{"ref": "main", "path": "src"})
if err != nil {
t.Fatalf("files with ref failed: %v", err)
}
}
func TestRepoFilesHTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server error"))
}))
defer server.Close()
err := runShortcut(t, server, "files", nil)
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
// --- tags ---
func TestRepoTags(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Fatalf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/owner/repo/tags.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("page") != "1" {
t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page"))
}
if r.URL.Query().Get("limit") != "20" {
t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit"))
}
writeJSON(w, map[string]interface{}{
"total_count": float64(1),
"data": []interface{}{map[string]interface{}{"name": "v1.0.0"}},
})
}))
defer server.Close()
err := runShortcut(t, server, "tags", map[string]string{"page": "1", "limit": "20"})
if err != nil {
t.Fatalf("tags failed: %v", err)
}
}
func TestRepoTagsHTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server error"))
}))
defer server.Close()
err := runShortcut(t, server, "tags", map[string]string{"page": "1", "limit": "20"})
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}
// --- commits ---
func TestRepoCommits(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Fatalf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/owner/repo/commits.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.Query().Get("page") != "1" {
t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page"))
}
if r.URL.Query().Get("limit") != "20" {
t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit"))
}
writeJSON(w, map[string]interface{}{
"total_count": float64(1),
"data": []interface{}{
map[string]interface{}{"sha": "abc123", "message": "initial commit"},
},
})
}))
defer server.Close()
err := runShortcut(t, server, "commits", map[string]string{"page": "1", "limit": "20"})
if err != nil {
t.Fatalf("commits failed: %v", err)
}
}
func TestRepoCommitsWithFilters(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("sha") != "main" {
t.Fatalf("expected sha=main, got %s", r.URL.Query().Get("sha"))
}
if r.URL.Query().Get("path") != "src/main.go" {
t.Fatalf("expected path=src/main.go, got %s", r.URL.Query().Get("path"))
}
writeJSON(w, map[string]interface{}{
"total_count": float64(1),
"data": []interface{}{},
})
}))
defer server.Close()
err := runShortcut(t, server, "commits", map[string]string{
"sha": "main",
"path": "src/main.go",
"page": "1",
"limit": "20",
})
if err != nil {
t.Fatalf("commits with filters failed: %v", err)
}
}
func TestRepoCommitsHTTPError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server error"))
}))
defer server.Close()
err := runShortcut(t, server, "commits", map[string]string{"page": "1", "limit": "20"})
if err == nil {
t.Fatal("expected error for HTTP 500")
}
}