Compare commits
1 Commits
master
...
feat/code-
| Author | SHA1 | Date |
|---|---|---|
|
|
d89f6f6d65 |
|
|
@ -0,0 +1,11 @@
|
|||
# Code read shortcuts
|
||||
|
||||
Adds `gitlink-cli code-read` for read-only repository code browsing.
|
||||
|
||||
Covered OpenAPI:
|
||||
|
||||
- `GET /api/{owner}/{repo}/files.json`
|
||||
- `GET /api/{owner}/{repo}/entries.json`
|
||||
- `GET /api/{owner}/{repo}/sub_entries.json`
|
||||
- `GET /api/v1/{owner}/{repo}/git/trees/{sha}.json`
|
||||
- `GET /api/v1/{owner}/{repo}/git/blobs/{sha}.json`
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package coderead
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{Name: "files", Description: "List all repository files", Flags: []common.Flag{{Name: "search", Short: "s", Usage: "Search keyword"}, {Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"}}, Run: runFiles},
|
||||
{Name: "entries", Description: "List root code entries", Flags: []common.Flag{{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"}}, Run: runEntries},
|
||||
{Name: "sub-entry", Description: "View a sub directory or file", Flags: []common.Flag{{Name: "path", Short: "p", Usage: "File or directory path", Required: true}, {Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"}}, Run: runSubEntry},
|
||||
{Name: "tree", Description: "View git tree", Flags: []common.Flag{{Name: "sha", Short: "s", Usage: "Tree SHA, branch, or tag", Required: true}, {Name: "recursive", Usage: "Show recursively", Bool: true, Default: "false"}, {Name: "page", Short: "p", Usage: "Page number"}, {Name: "limit", Short: "l", Usage: "Page size"}}, Run: runTree},
|
||||
{Name: "blob", Description: "View git blob content", Flags: []common.Flag{{Name: "sha", Short: "s", Usage: "Blob SHA", Required: true}}, Run: runBlob},
|
||||
}
|
||||
}
|
||||
|
||||
func runFiles(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
add(q, "search", ctx.Arg("search"))
|
||||
add(q, "ref", ctx.Arg("ref"))
|
||||
return get(ctx, fmt.Sprintf("/%s/%s/files", ctx.Owner, ctx.Repo), q)
|
||||
}
|
||||
func runEntries(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
add(q, "ref", ctx.Arg("ref"))
|
||||
return get(ctx, fmt.Sprintf("/%s/%s/entries", ctx.Owner, ctx.Repo), q)
|
||||
}
|
||||
func runSubEntry(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := require(ctx, "path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{"filepath": {path}}
|
||||
add(q, "ref", ctx.Arg("ref"))
|
||||
return get(ctx, fmt.Sprintf("/%s/%s/sub_entries", ctx.Owner, ctx.Repo), q)
|
||||
}
|
||||
func runTree(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
sha, err := require(ctx, "sha")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
if parseBool(ctx.Arg("recursive")) {
|
||||
q.Set("recursive", "true")
|
||||
}
|
||||
add(q, "page", ctx.Arg("page"))
|
||||
add(q, "limit", ctx.Arg("limit"))
|
||||
return get(ctx, fmt.Sprintf("/v1/%s/%s/git/trees/%s", ctx.Owner, ctx.Repo, url.PathEscape(sha)), q)
|
||||
}
|
||||
func runBlob(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
sha, err := require(ctx, "sha")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return get(ctx, fmt.Sprintf("/v1/%s/%s/git/blobs/%s", ctx.Owner, ctx.Repo, url.PathEscape(sha)), nil)
|
||||
}
|
||||
func get(ctx *common.RuntimeContext, path string, q url.Values) error {
|
||||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
func require(ctx *common.RuntimeContext, name string) (string, error) {
|
||||
v, err := ctx.RequireArg(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return "", fmt.Errorf("%s cannot be empty", name)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
func add(q url.Values, k, v string) {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
q.Set(k, strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
func parseBool(v string) bool { return strings.EqualFold(strings.TrimSpace(v), "true") }
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package coderead
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func runCR(t *testing.T, s *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
var sc *common.Shortcut
|
||||
for _, x := range Shortcuts() {
|
||||
if x.Name == name {
|
||||
sc = x
|
||||
}
|
||||
}
|
||||
if sc == nil {
|
||||
t.Fatalf("missing %s", name)
|
||||
}
|
||||
return sc.Run(&common.RuntimeContext{Client: &client.Client{HTTP: s.Client(), BaseURL: s.URL}, Owner: "alice", Repo: "repo", Args: args, Format: "json", Tr: i18n.Default()})
|
||||
}
|
||||
func wj(t *testing.T, w http.ResponseWriter, v interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
func TestCodeReadPaths(t *testing.T) {
|
||||
paths := []string{}
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
paths = append(paths, r.Method+" "+r.URL.Path+"?"+r.URL.RawQuery)
|
||||
wj(t, w, map[string]interface{}{"status": 0})
|
||||
}))
|
||||
defer s.Close()
|
||||
_ = runCR(t, s, "files", map[string]string{"search": "readme", "ref": "main"})
|
||||
_ = runCR(t, s, "entries", map[string]string{"ref": "main"})
|
||||
_ = runCR(t, s, "sub-entry", map[string]string{"path": "docs", "ref": "main"})
|
||||
_ = runCR(t, s, "tree", map[string]string{"sha": "main", "recursive": "true", "page": "2"})
|
||||
_ = runCR(t, s, "blob", map[string]string{"sha": "abc"})
|
||||
want := []string{"GET /alice/repo/files.json?ref=main&search=readme", "GET /alice/repo/entries.json?ref=main", "GET /alice/repo/sub_entries.json?filepath=docs&ref=main", "GET /v1/alice/repo/git/trees/main.json?page=2&recursive=true", "GET /v1/alice/repo/git/blobs/abc.json?"}
|
||||
for i := range want {
|
||||
if paths[i] != want[i] {
|
||||
t.Fatalf("paths=%#v want %q", paths, want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestSubEntryRequiresPath(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("unexpected API") }))
|
||||
defer s.Close()
|
||||
if err := runCR(t, s, "sub-entry", nil); err == nil {
|
||||
t.Fatal("expected missing path")
|
||||
}
|
||||
}
|
||||
|
|
@ -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/coderead"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/compare"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/dataset"
|
||||
|
|
@ -36,6 +37,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
}
|
||||
groups := map[string][]*common.Shortcut{
|
||||
"repo": repo.Shortcuts(tr),
|
||||
"code-read": coderead.Shortcuts(),
|
||||
"issue": issue.Shortcuts(tr),
|
||||
"label": label.Shortcuts(),
|
||||
"license": license.Shortcuts(),
|
||||
|
|
@ -60,6 +62,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
|
||||
descriptions := map[string]string{
|
||||
"repo": tr.T("cmd.repo.short"),
|
||||
"code-read": "Repository code browsing 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",
|
||||
"dataset", "health", "ignore", "code-read",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# GitLink Code Read Skill
|
||||
|
||||
Use for read-only repository code browsing.
|
||||
|
||||
```bash
|
||||
gitlink-cli code-read +files --owner <owner> --repo <repo> --search README --ref master --format json
|
||||
gitlink-cli code-read +entries --owner <owner> --repo <repo> --ref master --format json
|
||||
gitlink-cli code-read +sub-entry --owner <owner> --repo <repo> --path docs --ref master --format json
|
||||
gitlink-cli code-read +tree --owner <owner> --repo <repo> --sha master --recursive --format json
|
||||
gitlink-cli code-read +blob --owner <owner> --repo <repo> --sha <blob-sha> --format json
|
||||
```
|
||||
|
||||
All commands are read-only.
|
||||
Loading…
Reference in New Issue