diff --git a/README.md b/README.md index 670808c..9899f1e 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,9 @@ gitlink-cli repo +list # View repository info gitlink-cli repo +info --owner Gitlink --repo forgeplus +# Read repository README +gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master + # Create a repository gitlink-cli repo +create -n my-project -d "Project description" diff --git a/README.zh-CN.md b/README.zh-CN.md index f426948..496b0eb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -162,6 +162,9 @@ gitlink-cli repo +list # 查看仓库信息 gitlink-cli repo +info --owner Gitlink --repo forgeplus +# 读取仓库 README +gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master + # 创建仓库 gitlink-cli repo +create -n my-project -d "项目描述" diff --git a/internal/client/client.go b/internal/client/client.go index d147d96..831caa7 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -42,6 +42,8 @@ func New() (*Client, error) { } func (c *Client) Do(method, path string, body interface{}, query url.Values) (*output.Envelope, error) { + path = normalizeAPIPath(c.BaseURL, path) + // Append .json suffix if not already present (GitLink API convention) // Handle paths that may already contain query strings (e.g., /path?key=val) if idx := strings.Index(path, "?"); idx != -1 { @@ -158,6 +160,18 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o return output.SuccessEnvelope(raw, meta), nil } +func normalizeAPIPath(baseURL, path string) string { + if strings.HasSuffix(strings.TrimRight(baseURL, "/"), "/api") { + switch { + case path == "/api": + return "" + case strings.HasPrefix(path, "/api/"): + return strings.TrimPrefix(path, "/api") + } + } + return path +} + func (c *Client) Get(path string, query url.Values) (*output.Envelope, error) { return c.Do("GET", path, nil, query) } diff --git a/internal/client/client_test.go b/internal/client/client_test.go new file mode 100644 index 0000000..87fb91d --- /dev/null +++ b/internal/client/client_test.go @@ -0,0 +1,27 @@ +package client + +import "testing" + +func TestNormalizeAPIPathStripsDuplicateAPIPrefix(t *testing.T) { + got := normalizeAPIPath("https://www.gitlink.org.cn/api", "/api/v1/repos/Gitlink/gitlink-cli/contents/README.md") + want := "/v1/repos/Gitlink/gitlink-cli/contents/README.md" + if got != want { + t.Fatalf("normalizeAPIPath() = %q, want %q", got, want) + } +} + +func TestNormalizeAPIPathKeepsRegularPath(t *testing.T) { + got := normalizeAPIPath("https://www.gitlink.org.cn/api", "/projects") + want := "/projects" + if got != want { + t.Fatalf("normalizeAPIPath() = %q, want %q", got, want) + } +} + +func TestNormalizeAPIPathKeepsAPIPrefixForNonAPIBaseURL(t *testing.T) { + got := normalizeAPIPath("https://www.gitlink.org.cn", "/api/v1/repos/Gitlink/gitlink-cli") + want := "/api/v1/repos/Gitlink/gitlink-cli" + if got != want { + t.Fatalf("normalizeAPIPath() = %q, want %q", got, want) + } +} diff --git a/shortcuts/repo/repo.go b/shortcuts/repo/repo.go index 75091a4..4d621ad 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -52,6 +52,31 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "readme", + Description: "Show repository README content", + Flags: []common.Flag{ + {Name: "ref", Usage: "Branch, tag, or commit SHA"}, + {Name: "path", Usage: "README directory path"}, + }, + 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 path := ctx.Arg("path"); path != "" { + q.Set("filepath", path) + } + env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/readme", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, { Name: "create", Description: "Create a new repository", diff --git a/shortcuts/repo/repo_test.go b/shortcuts/repo/repo_test.go new file mode 100644 index 0000000..41305c4 --- /dev/null +++ b/shortcuts/repo/repo_test.go @@ -0,0 +1,63 @@ +package repo + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func TestRepoReadmeUsesRepositoryReadmeEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/owner/repo/readme.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + if got := r.URL.Query().Get("ref"); got != "main" { + t.Fatalf("ref query = %q, want main", got) + } + if got := r.URL.Query().Get("filepath"); got != "docs" { + t.Fatalf("filepath query = %q, want docs", got) + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]interface{}{ + "type": "file", + "name": "README.md", + "content": "# docs\n", + }); err != nil { + t.Fatalf("write response: %v", err) + } + })) + defer server.Close() + + err := runRepoShortcut(server, "readme", map[string]string{ + "ref": "main", + "path": "docs", + }) + if err != nil { + t.Fatalf("readme shortcut failed: %v", err) + } +} + +func runRepoShortcut(server *httptest.Server, name string, args map[string]string) error { + for _, shortcut := range Shortcuts() { + if shortcut.Name != name { + continue + } + ctx := &common.RuntimeContext{ + Client: &client.Client{ + HTTP: server.Client(), + BaseURL: server.URL, + }, + Owner: "owner", + Repo: "repo", + Format: "json", + Args: args, + } + return shortcut.Run(ctx) + } + return fmt.Errorf("shortcut %q not found", name) +}