Compare commits
1 Commits
master
...
feat/code-
| Author | SHA1 | Date |
|---|---|---|
|
|
3b3cd1c33c |
28
README.md
28
README.md
|
|
@ -96,6 +96,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| Category | Capabilities |
|
||||
|----------|-------------|
|
||||
| 📦 Repo | List, create, fork, delete repositories, view repo info, insights, and interactions |
|
||||
| Code | Browse repository files, trees, blobs, commits, blame, and tags |
|
||||
| 🐛 Issue | Create, update, close, batch close, comment on issues |
|
||||
| 🔖 Label | Create, list, update, delete issue labels |
|
||||
| 🔀 PR | Create, merge, review pull requests, view changed files |
|
||||
|
|
@ -246,6 +247,32 @@ gitlink-cli repo +create -n my-project -d "Project description"
|
|||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### Code Browsing
|
||||
|
||||
```bash
|
||||
# Search repository files
|
||||
gitlink-cli code +files --owner Gitlink --repo forgeplus --search README --ref master
|
||||
|
||||
# List root entries or inspect a path
|
||||
gitlink-cli code +entries --owner Gitlink --repo forgeplus --ref master
|
||||
gitlink-cli code +sub-entries --owner Gitlink --repo forgeplus --path docs --ref master
|
||||
|
||||
# Read git object data
|
||||
gitlink-cli code +tree --owner Gitlink --repo forgeplus --sha master --recursive
|
||||
gitlink-cli code +blob --owner Gitlink --repo forgeplus --sha <blob-sha>
|
||||
|
||||
# Inspect commits
|
||||
gitlink-cli code +commits --owner Gitlink --repo forgeplus --sha master
|
||||
gitlink-cli code +commit-files --owner Gitlink --repo forgeplus --sha <commit-sha>
|
||||
gitlink-cli code +commit-diff --owner Gitlink --repo forgeplus --sha <commit-sha>
|
||||
gitlink-cli code +blame --owner Gitlink --repo forgeplus --sha master --path README.md
|
||||
|
||||
# Work with tags
|
||||
gitlink-cli code +tags --owner Gitlink --repo forgeplus
|
||||
gitlink-cli code +tag --owner Gitlink --repo forgeplus --name v1.0.0
|
||||
gitlink-cli code +delete-tag --owner Gitlink --repo forgeplus --name v1.0.0 --dry-run
|
||||
```
|
||||
|
||||
### Webhook Management
|
||||
|
||||
```bash
|
||||
|
|
@ -638,6 +665,7 @@ See [skills/README.md](skills/README.md) for details.
|
|||
|-------|-------------|
|
||||
| `gitlink-shared` | Authentication, global parameters, safety rules, API notes |
|
||||
| `gitlink-repo` | Repository operations (create, view, delete, fork, insights, etc.) |
|
||||
| `gitlink-code` | Repository code browsing (files, trees, blobs, commits, blame, tags) |
|
||||
| `gitlink-issue` | Issue operations (create, update, close, comment, etc.) |
|
||||
| `gitlink-pr` | Pull request operations (create, merge, review, etc.) |
|
||||
| `gitlink-member` | Repository member and invite link management |
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@
|
|||
| 分类 | 能力 |
|
||||
|------|------|
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息、洞察数据和互动状态 |
|
||||
| 代码 | 浏览仓库文件、目录树、Blob、提交、Blame 和标签 |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
|
||||
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
|
|
@ -257,6 +258,32 @@ gitlink-cli repo +create -n my-project -d "项目描述"
|
|||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### 代码浏览
|
||||
|
||||
```bash
|
||||
# 搜索仓库文件
|
||||
gitlink-cli code +files --owner Gitlink --repo forgeplus --search README --ref master
|
||||
|
||||
# 列出根目录或查看指定路径
|
||||
gitlink-cli code +entries --owner Gitlink --repo forgeplus --ref master
|
||||
gitlink-cli code +sub-entries --owner Gitlink --repo forgeplus --path docs --ref master
|
||||
|
||||
# 读取 Git 对象数据
|
||||
gitlink-cli code +tree --owner Gitlink --repo forgeplus --sha master --recursive
|
||||
gitlink-cli code +blob --owner Gitlink --repo forgeplus --sha <blob-sha>
|
||||
|
||||
# 查看提交信息
|
||||
gitlink-cli code +commits --owner Gitlink --repo forgeplus --sha master
|
||||
gitlink-cli code +commit-files --owner Gitlink --repo forgeplus --sha <commit-sha>
|
||||
gitlink-cli code +commit-diff --owner Gitlink --repo forgeplus --sha <commit-sha>
|
||||
gitlink-cli code +blame --owner Gitlink --repo forgeplus --sha master --path README.md
|
||||
|
||||
# 查看标签
|
||||
gitlink-cli code +tags --owner Gitlink --repo forgeplus
|
||||
gitlink-cli code +tag --owner Gitlink --repo forgeplus --name v1.0.0
|
||||
gitlink-cli code +delete-tag --owner Gitlink --repo forgeplus --name v1.0.0 --dry-run
|
||||
```
|
||||
|
||||
### Webhook 管理
|
||||
|
||||
```bash
|
||||
|
|
@ -517,6 +544,7 @@ git push gitlink
|
|||
|-------|------|
|
||||
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
|
||||
| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) |
|
||||
| `gitlink-code` | 代码浏览(文件、目录树、Blob、提交、Blame、标签) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
|
||||
| `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) |
|
||||
| `gitlink-member` | 仓库成员与邀请链接管理 |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
# Code OpenAPI Shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
This change adds a dedicated `code` shortcut group for repository source-code
|
||||
inspection APIs. The group keeps code browsing separate from repository
|
||||
administration commands, which reduces coupling with existing `repo` shortcuts.
|
||||
|
||||
## Added shortcuts
|
||||
|
||||
| Shortcut | API |
|
||||
|----------|-----|
|
||||
| `code +files` | `GET /api/{owner}/{repo}/files.json` |
|
||||
| `code +entries` | `GET /api/{owner}/{repo}/entries.json` |
|
||||
| `code +sub-entries` | `GET /api/{owner}/{repo}/sub_entries.json` |
|
||||
| `code +tree` | `GET /api/v1/{owner}/{repo}/git/trees/{sha}.json` |
|
||||
| `code +blob` | `GET /api/v1/{owner}/{repo}/git/blobs/{sha}.json` |
|
||||
| `code +commits` | `GET /api/v1/{owner}/{repo}/commits.json` |
|
||||
| `code +commit-files` | `GET /api/v1/{owner}/{repo}/commits/{sha}/files.json` |
|
||||
| `code +commit-diff` | `GET /api/v1/{owner}/{repo}/commits/{sha}/diff.json` |
|
||||
| `code +blame` | `GET /api/v1/{owner}/{repo}/blame.json` |
|
||||
| `code +tags` | `GET /api/v1/{owner}/{repo}/tags.json` |
|
||||
| `code +tag` | `GET /api/v1/{owner}/{repo}/tags/{name}.json` |
|
||||
| `code +delete-tag` | `DELETE /api/v1/{owner}/{repo}/tags/{tag}.json` |
|
||||
|
||||
`code +delete-tag` supports `--dry-run` so users can preview the exact delete
|
||||
request before changing repository state.
|
||||
|
||||
## Submitter
|
||||
|
||||
Wang Yue
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "files",
|
||||
Description: "Search repository files",
|
||||
Flags: []common.Flag{
|
||||
{Name: "search", Short: "s", Usage: "Search keyword"},
|
||||
{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
setQueryIfPresent(q, ctx, "search", "search")
|
||||
setQueryIfPresent(q, ctx, "ref", "ref")
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "entries",
|
||||
Description: "List repository root entries",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
setQueryIfPresent(q, ctx, "ref", "ref")
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/entries", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "sub-entries",
|
||||
Description: "Show a repository directory or file entry",
|
||||
Flags: []common.Flag{
|
||||
{Name: "path", Short: "p", Usage: "Repository-relative file or directory path", Required: true},
|
||||
{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
filepath, err := ctx.RequireArg("path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("filepath", filepath)
|
||||
setQueryIfPresent(q, ctx, "ref", "ref")
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tree",
|
||||
Description: "List a git tree",
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: "Branch, tag, commit SHA, or tree SHA", Required: true},
|
||||
{Name: "recursive", Short: "R", Usage: "Show recursive entries", Bool: true, Default: "false"},
|
||||
{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 := pageLimitQuery(ctx)
|
||||
if ctx.Arg("recursive") == "true" {
|
||||
q.Set("recursive", "true")
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/git/trees/%s", codeV1RepoPath(ctx), url.PathEscape(sha)), q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "blob",
|
||||
Description: "Show a git blob",
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: "Blob 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/git/blobs/%s", codeV1RepoPath(ctx), url.PathEscape(sha)), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "commits",
|
||||
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 := pageLimitQuery(ctx)
|
||||
setQueryIfPresent(q, ctx, "sha", "sha")
|
||||
env, err := ctx.CallAPIWithQuery("GET", codeV1RepoPath(ctx)+"/commits", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "commit-files",
|
||||
Description: "List files changed by a commit",
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: "Commit SHA", Required: true},
|
||||
{Name: "file", 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 := pageLimitQuery(ctx)
|
||||
setQueryIfPresent(q, ctx, "file", "filepath")
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/commits/%s/files", codeV1RepoPath(ctx), url.PathEscape(sha)), q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "commit-diff",
|
||||
Description: "Show a commit diff",
|
||||
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", codeV1RepoPath(ctx), url.PathEscape(sha)), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "blame",
|
||||
Description: "Show file blame information",
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: "Branch, tag, or commit SHA", Required: true},
|
||||
{Name: "path", Short: "p", Usage: "Repository-relative 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("path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("sha", sha)
|
||||
q.Set("filepath", filepath)
|
||||
env, err := ctx.CallAPIWithQuery("GET", codeV1RepoPath(ctx)+"/blame", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tags",
|
||||
Description: "List repository tags",
|
||||
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
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", codeV1RepoPath(ctx)+"/tags", pageLimitQuery(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tag",
|
||||
Description: "Show repository tag details",
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: "Tag name", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/tags/%s", codeV1RepoPath(ctx), url.PathEscape(name)), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete-tag",
|
||||
Description: "Delete a repository tag",
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: "Tag name", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the delete request without changing repository state", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("%s/tags/%s", codeV1RepoPath(ctx), url.PathEscape(name))
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"dry_run": true,
|
||||
"action": "delete_tag",
|
||||
"tag": name,
|
||||
"method": "DELETE",
|
||||
"path": path,
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func codeV1RepoPath(ctx *common.RuntimeContext) string {
|
||||
return "/v1" + ctx.RepoPath()
|
||||
}
|
||||
|
||||
func setQueryIfPresent(q url.Values, ctx *common.RuntimeContext, flagName, queryName string) {
|
||||
if value := ctx.Arg(flagName); value != "" {
|
||||
q.Set(queryName, value)
|
||||
}
|
||||
}
|
||||
|
||||
func pageLimitQuery(ctx *common.RuntimeContext) url.Values {
|
||||
q := url.Values{}
|
||||
setQueryIfPresent(q, ctx, "page", "page")
|
||||
setQueryIfPresent(q, ctx, "limit", "limit")
|
||||
return q
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
package code
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestCodeShortcutsRouteToOpenAPIEndpoints(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args map[string]string
|
||||
method string
|
||||
path string
|
||||
query map[string]string
|
||||
}{
|
||||
{
|
||||
name: "files",
|
||||
args: map[string]string{"search": "README", "ref": "main"},
|
||||
method: "GET",
|
||||
path: "/owner/repo/files.json",
|
||||
query: map[string]string{"search": "README", "ref": "main"},
|
||||
},
|
||||
{
|
||||
name: "entries",
|
||||
args: map[string]string{"ref": "main"},
|
||||
method: "GET",
|
||||
path: "/owner/repo/entries.json",
|
||||
query: map[string]string{"ref": "main"},
|
||||
},
|
||||
{
|
||||
name: "sub-entries",
|
||||
args: map[string]string{"path": "docs/README.md", "ref": "main"},
|
||||
method: "GET",
|
||||
path: "/owner/repo/sub_entries.json",
|
||||
query: map[string]string{"filepath": "docs/README.md", "ref": "main"},
|
||||
},
|
||||
{
|
||||
name: "tree",
|
||||
args: map[string]string{"sha": "main", "recursive": "true", "page": "2", "limit": "50"},
|
||||
method: "GET",
|
||||
path: "/v1/owner/repo/git/trees/main.json",
|
||||
query: map[string]string{"recursive": "true", "page": "2", "limit": "50"},
|
||||
},
|
||||
{
|
||||
name: "blob",
|
||||
args: map[string]string{"sha": "abc123"},
|
||||
method: "GET",
|
||||
path: "/v1/owner/repo/git/blobs/abc123.json",
|
||||
},
|
||||
{
|
||||
name: "commits",
|
||||
args: map[string]string{"sha": "main", "page": "1", "limit": "20"},
|
||||
method: "GET",
|
||||
path: "/v1/owner/repo/commits.json",
|
||||
query: map[string]string{"sha": "main", "page": "1", "limit": "20"},
|
||||
},
|
||||
{
|
||||
name: "commit-files",
|
||||
args: map[string]string{"sha": "abc123", "file": "README.md", "page": "1", "limit": "20"},
|
||||
method: "GET",
|
||||
path: "/v1/owner/repo/commits/abc123/files.json",
|
||||
query: map[string]string{"filepath": "README.md", "page": "1", "limit": "20"},
|
||||
},
|
||||
{
|
||||
name: "commit-diff",
|
||||
args: map[string]string{"sha": "abc123"},
|
||||
method: "GET",
|
||||
path: "/v1/owner/repo/commits/abc123/diff.json",
|
||||
},
|
||||
{
|
||||
name: "blame",
|
||||
args: map[string]string{"sha": "main", "path": "README.md"},
|
||||
method: "GET",
|
||||
path: "/v1/owner/repo/blame.json",
|
||||
query: map[string]string{"sha": "main", "filepath": "README.md"},
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
args: map[string]string{"page": "1", "limit": "20"},
|
||||
method: "GET",
|
||||
path: "/v1/owner/repo/tags.json",
|
||||
query: map[string]string{"page": "1", "limit": "20"},
|
||||
},
|
||||
{
|
||||
name: "tag",
|
||||
args: map[string]string{"name": "v1.0"},
|
||||
method: "GET",
|
||||
path: "/v1/owner/repo/tags/v1.0.json",
|
||||
},
|
||||
{
|
||||
name: "delete-tag",
|
||||
args: map[string]string{"name": "v1.0"},
|
||||
method: "DELETE",
|
||||
path: "/v1/owner/repo/tags/v1.0.json",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
called := false
|
||||
server := newCodeTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != tc.method || r.URL.Path != tc.path {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
assertCodeQuery(t, r, tc.query)
|
||||
called = true
|
||||
writeCodeJSON(t, w, map[string]interface{}{"ok": true})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runCodeShortcut(server, tc.name, tc.args)
|
||||
if err != nil {
|
||||
t.Fatalf("%s shortcut failed: %v", tc.name, err)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("expected API request")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodeDeleteTagDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := newCodeTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runCodeShortcut(server, "delete-tag", map[string]string{
|
||||
"name": "v1.0",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("delete-tag dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodeShortcutsValidateRequiredArgs(t *testing.T) {
|
||||
server := newCodeTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args map[string]string
|
||||
want string
|
||||
}{
|
||||
{name: "sub-entries", args: map[string]string{}, want: "--path"},
|
||||
{name: "tree", args: map[string]string{}, want: "--sha"},
|
||||
{name: "blob", args: map[string]string{}, want: "--sha"},
|
||||
{name: "commit-files", args: map[string]string{}, want: "--sha"},
|
||||
{name: "commit-diff", args: map[string]string{}, want: "--sha"},
|
||||
{name: "blame", args: map[string]string{"sha": "main"}, want: "--path"},
|
||||
{name: "tag", args: map[string]string{}, want: "--name"},
|
||||
{name: "delete-tag", args: map[string]string{}, want: "--name"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := runCodeShortcut(server, tc.name, tc.args)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("error = %q, want it to mention %s", err.Error(), tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runCodeShortcut(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,
|
||||
Tr: i18n.Default(),
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
return fmt.Errorf("shortcut %q not found", name)
|
||||
}
|
||||
|
||||
func newCodeTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
func assertCodeQuery(t *testing.T, r *http.Request, want map[string]string) {
|
||||
t.Helper()
|
||||
query := r.URL.Query()
|
||||
if len(query) != len(want) {
|
||||
t.Fatalf("query = %v, want %v", query, want)
|
||||
}
|
||||
for key, value := range want {
|
||||
if got := query.Get(key); got != value {
|
||||
t.Fatalf("query %s = %q, want %q", key, got, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeCodeJSON(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("failed to write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -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/code"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/compare"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/health"
|
||||
|
|
@ -46,6 +47,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"user": user.Shortcuts(tr),
|
||||
"search": search.Shortcuts(tr),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"code": code.Shortcuts(),
|
||||
"compare": compare.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(tr),
|
||||
"health": health.Shortcuts(tr),
|
||||
|
|
@ -67,6 +69,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"user": tr.T("cmd.user.short"),
|
||||
"search": tr.T("cmd.search.short"),
|
||||
"ci": tr.T("cmd.ci.short"),
|
||||
"code": "Repository code browsing",
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"webhook": tr.T("cmd.webhook.short"),
|
||||
"health": "Project health data collection",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
|
|||
"repo", "issue", "label", "license", "pr", "release", "branch",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"health",
|
||||
"health", "code",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
name: gitlink-code
|
||||
version: 1.0.0
|
||||
description: "Repository code browsing: files, entries, git trees, blobs, commits, blame, and tags. Use when users need to inspect GitLink repository source code metadata."
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli code --help"
|
||||
---
|
||||
|
||||
# gitlink-code
|
||||
|
||||
Read `../gitlink-shared/SKILL.md` first for authentication, global flags, and safety rules.
|
||||
|
||||
Write operations must be confirmed by the user before execution. `code +delete-tag`
|
||||
supports `--dry-run` and should be previewed before deleting a tag.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Shortcut | Purpose | Auth |
|
||||
|----------|---------|------|
|
||||
| `code +files` | Search repository files | No for public repos |
|
||||
| `code +entries` | List root code entries | No for public repos |
|
||||
| `code +sub-entries` | Show a directory or file entry | No for public repos |
|
||||
| `code +tree` | List a git tree by branch, tag, commit, or tree SHA | No for public repos |
|
||||
| `code +blob` | Read a git blob by SHA | No for public repos |
|
||||
| `code +commits` | List commits | No for public repos |
|
||||
| `code +commit-files` | List files changed by one commit | No for public repos |
|
||||
| `code +commit-diff` | Show one commit diff | No for public repos |
|
||||
| `code +blame` | Show file blame data | No for public repos |
|
||||
| `code +tags` | List repository tags | No for public repos |
|
||||
| `code +tag` | Show tag details | No for public repos |
|
||||
| `code +delete-tag` | Delete a tag | Yes |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
gitlink-cli code +files --owner Gitlink --repo forgeplus --search README --ref master
|
||||
gitlink-cli code +entries --owner Gitlink --repo forgeplus --ref master
|
||||
gitlink-cli code +sub-entries --owner Gitlink --repo forgeplus --path docs --ref master
|
||||
|
||||
gitlink-cli code +tree --owner Gitlink --repo forgeplus --sha master --recursive
|
||||
gitlink-cli code +blob --owner Gitlink --repo forgeplus --sha <blob-sha>
|
||||
|
||||
gitlink-cli code +commits --owner Gitlink --repo forgeplus --sha master
|
||||
gitlink-cli code +commit-files --owner Gitlink --repo forgeplus --sha <commit-sha>
|
||||
gitlink-cli code +commit-diff --owner Gitlink --repo forgeplus --sha <commit-sha>
|
||||
gitlink-cli code +blame --owner Gitlink --repo forgeplus --sha master --path README.md
|
||||
|
||||
gitlink-cli code +tags --owner Gitlink --repo forgeplus
|
||||
gitlink-cli code +tag --owner Gitlink --repo forgeplus --name v1.0.0
|
||||
gitlink-cli code +delete-tag --owner Gitlink --repo forgeplus --name v1.0.0 --dry-run
|
||||
```
|
||||
|
||||
## API Mapping
|
||||
|
||||
| Shortcut | API |
|
||||
|----------|-----|
|
||||
| `code +files` | `GET /api/{owner}/{repo}/files.json` |
|
||||
| `code +entries` | `GET /api/{owner}/{repo}/entries.json` |
|
||||
| `code +sub-entries` | `GET /api/{owner}/{repo}/sub_entries.json` |
|
||||
| `code +tree` | `GET /api/v1/{owner}/{repo}/git/trees/{sha}.json` |
|
||||
| `code +blob` | `GET /api/v1/{owner}/{repo}/git/blobs/{sha}.json` |
|
||||
| `code +commits` | `GET /api/v1/{owner}/{repo}/commits.json` |
|
||||
| `code +commit-files` | `GET /api/v1/{owner}/{repo}/commits/{sha}/files.json` |
|
||||
| `code +commit-diff` | `GET /api/v1/{owner}/{repo}/commits/{sha}/diff.json` |
|
||||
| `code +blame` | `GET /api/v1/{owner}/{repo}/blame.json` |
|
||||
| `code +tags` | `GET /api/v1/{owner}/{repo}/tags.json` |
|
||||
| `code +tag` | `GET /api/v1/{owner}/{repo}/tags/{name}.json` |
|
||||
| `code +delete-tag` | `DELETE /api/v1/{owner}/{repo}/tags/{tag}.json` |
|
||||
Loading…
Reference in New Issue