Compare commits

...

1 Commits

Author SHA1 Message Date
wangyue789 3b3cd1c33c feat(code): add repository code OpenAPI shortcuts 2026-06-09 08:10:31 +08:00
8 changed files with 697 additions and 1 deletions

View File

@ -96,6 +96,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
| Category | Capabilities | | Category | Capabilities |
|----------|-------------| |----------|-------------|
| 📦 Repo | List, create, fork, delete repositories, view repo info, insights, and interactions | | 📦 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 | | 🐛 Issue | Create, update, close, batch close, comment on issues |
| 🔖 Label | Create, list, update, delete issue labels | | 🔖 Label | Create, list, update, delete issue labels |
| 🔀 PR | Create, merge, review pull requests, view changed files | | 🔀 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 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 ### Webhook Management
```bash ```bash
@ -638,6 +665,7 @@ See [skills/README.md](skills/README.md) for details.
|-------|-------------| |-------|-------------|
| `gitlink-shared` | Authentication, global parameters, safety rules, API notes | | `gitlink-shared` | Authentication, global parameters, safety rules, API notes |
| `gitlink-repo` | Repository operations (create, view, delete, fork, insights, etc.) | | `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-issue` | Issue operations (create, update, close, comment, etc.) |
| `gitlink-pr` | Pull request operations (create, merge, review, etc.) | | `gitlink-pr` | Pull request operations (create, merge, review, etc.) |
| `gitlink-member` | Repository member and invite link management | | `gitlink-member` | Repository member and invite link management |

View File

@ -96,6 +96,7 @@
| 分类 | 能力 | | 分类 | 能力 |
|------|------| |------|------|
| 📦 仓库 | 列出、创建、Fork、删除仓库查看仓库信息、洞察数据和互动状态 | | 📦 仓库 | 列出、创建、Fork、删除仓库查看仓库信息、洞察数据和互动状态 |
| 代码 | 浏览仓库文件、目录树、Blob、提交、Blame 和标签 |
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue | | 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 | | 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
| 🔀 PR | 创建、合并、Review Pull Request查看变更文件 | | 🔀 PR | 创建、合并、Review Pull Request查看变更文件 |
@ -257,6 +258,32 @@ gitlink-cli repo +create -n my-project -d "项目描述"
gitlink-cli repo +fork --owner Gitlink --repo forgeplus 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 管理 ### Webhook 管理
```bash ```bash
@ -517,6 +544,7 @@ git push gitlink
|-------|------| |-------|------|
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 | | `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
| `gitlink-repo` | 仓库操作创建、查看、删除、Fork、洞察数据等 | | `gitlink-repo` | 仓库操作创建、查看、删除、Fork、洞察数据等 |
| `gitlink-code` | 代码浏览文件、目录树、Blob、提交、Blame、标签 |
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) | | `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
| `gitlink-pr` | Pull Request 操作创建、合并、Review 等) | | `gitlink-pr` | Pull Request 操作创建、合并、Review 等) |
| `gitlink-member` | 仓库成员与邀请链接管理 | | `gitlink-member` | 仓库成员与邀请链接管理 |

View File

@ -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

313
shortcuts/code/code.go Normal file
View File

@ -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
}

223
shortcuts/code/code_test.go Normal file
View File

@ -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)
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts/branch" "github.com/gitlink-org/gitlink-cli/shortcuts/branch"
"github.com/gitlink-org/gitlink-cli/shortcuts/ci" "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/common"
"github.com/gitlink-org/gitlink-cli/shortcuts/compare" "github.com/gitlink-org/gitlink-cli/shortcuts/compare"
"github.com/gitlink-org/gitlink-cli/shortcuts/health" "github.com/gitlink-org/gitlink-cli/shortcuts/health"
@ -46,6 +47,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
"user": user.Shortcuts(tr), "user": user.Shortcuts(tr),
"search": search.Shortcuts(tr), "search": search.Shortcuts(tr),
"ci": ci.Shortcuts(tr), "ci": ci.Shortcuts(tr),
"code": code.Shortcuts(),
"compare": compare.Shortcuts(), "compare": compare.Shortcuts(),
"webhook": webhook.Shortcuts(tr), "webhook": webhook.Shortcuts(tr),
"health": health.Shortcuts(tr), "health": health.Shortcuts(tr),
@ -67,6 +69,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
"user": tr.T("cmd.user.short"), "user": tr.T("cmd.user.short"),
"search": tr.T("cmd.search.short"), "search": tr.T("cmd.search.short"),
"ci": tr.T("cmd.ci.short"), "ci": tr.T("cmd.ci.short"),
"code": "Repository code browsing",
"compare": "Compare branches, tags, or commits", "compare": "Compare branches, tags, or commits",
"webhook": tr.T("cmd.webhook.short"), "webhook": tr.T("cmd.webhook.short"),
"health": "Project health data collection", "health": "Project health data collection",

View File

@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
"repo", "issue", "label", "license", "pr", "release", "branch", "repo", "issue", "label", "license", "pr", "release", "branch",
"org", "user", "search", "ci", "workflow", "org", "user", "search", "ci", "workflow",
"compare", "member", "milestone", "pipeline", "webhook", "compare", "member", "milestone", "pipeline", "webhook",
"health", "health", "code",
} }
groupSet := map[string]bool{} groupSet := map[string]bool{}

View File

@ -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` |