feat: add repo tree shortcut #138

Closed
ohanabi wants to merge 1 commits from ohanabi/gitlink-cli:feat/repo-tree-shortcut into master
7 changed files with 193 additions and 2 deletions

View File

@ -151,6 +151,10 @@ gitlink-cli repo +list
# View repository info
gitlink-cli repo +info --owner Gitlink --repo forgeplus
# List repository files at root or a directory
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
# Create a repository
gitlink-cli repo +create -n my-project -d "Project description"

View File

@ -162,6 +162,10 @@ gitlink-cli repo +list
# 查看仓库信息
gitlink-cli repo +info --owner Gitlink --repo forgeplus
# 列出仓库根目录或指定目录文件
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
# 创建仓库
gitlink-cli repo +create -n my-project -d "项目描述"

View File

@ -40,7 +40,7 @@ gitlink-cli/
│ ├── common/
│ │ ├── types.go # Shortcut / Flag / RuntimeContext 定义
│ │ └── runner.go # CallAPI / PaginateAll / ResolveOwnerRepo
│ ├── repo/ # repo +create / +clone / +fork / +list / +info
│ ├── repo/ # repo +create / +clone / +fork / +list / +info / +tree
│ ├── issue/ # issue +list / +create / +view / +close / +comment
│ ├── pr/ # pr +list / +create / +view / +merge / +review
│ ├── release/ # release +list / +create / +download
@ -78,7 +78,7 @@ gitlink-cli/
| 领域 | Shortcuts | 数量 |
|------|-----------|------|
| repo | `+create` `+clone` `+fork` `+list` `+info` `+delete` `+settings` | 7 |
| repo | `+create` `+clone` `+fork` `+list` `+info` `+tree` `+delete` `+settings` | 8 |
| issue | `+list` `+create` `+view` `+update` `+close` `+comment` `+assign` `+label` | 8 |
| pr | `+list` `+create` `+view` `+merge` `+close` `+review` `+files` `+diff` | 8 |
| release | `+list` `+create` `+view` `+delete` `+download` | 5 |

View File

@ -52,6 +52,31 @@ func Shortcuts() []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "tree",
Description: "List repository files and directories",
Flags: []common.Flag{
{Name: "path", Short: "p", Usage: "Directory path to list (default: repository root)"},
{Name: "ref", Short: "r", Usage: "Branch, tag, or commit ref", Default: "master"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
ref := ctx.Arg("ref")
if ref == "" {
ref = "master"
}
q.Set("filepath", ctx.Arg("path"))
q.Set("ref", ref)
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "create",
Description: "Create a new repository",

111
shortcuts/repo/repo_test.go Normal file
View File

@ -0,0 +1,111 @@
package repo
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestRepoTreeListsRootOnDefaultRef(t *testing.T) {
var filepath string
var ref string
server := newRepoTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/sub_entries.json")
filepath = r.URL.Query().Get("filepath")
ref = r.URL.Query().Get("ref")
writeJSON(t, w, map[string]interface{}{
"entries": []map[string]interface{}{
{"name": "README.md", "type": "file"},
},
})
})
defer server.Close()
if err := runRepoShortcut(t, server, "tree", map[string]string{}); err != nil {
t.Fatalf("tree shortcut failed: %v", err)
}
assertEqual(t, filepath, "")
assertEqual(t, ref, "master")
}
func TestRepoTreeUsesPathAndRef(t *testing.T) {
var filepath string
var ref string
server := newRepoTestServer(t, func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/sub_entries.json")
filepath = r.URL.Query().Get("filepath")
ref = r.URL.Query().Get("ref")
writeJSON(t, w, map[string]interface{}{
"entries": []map[string]interface{}{
{"name": "main.go", "type": "file"},
},
})
})
defer server.Close()
if err := runRepoShortcut(t, server, "tree", map[string]string{"path": "cmd", "ref": "main"}); err != nil {
t.Fatalf("tree shortcut failed: %v", err)
}
assertEqual(t, filepath, "cmd")
assertEqual(t, ref, "main")
}
func runRepoShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
shortcut := findRepoShortcut(t, name)
ctx := &common.RuntimeContext{
Client: &client.Client{
HTTP: server.Client(),
BaseURL: server.URL,
},
Owner: "owner",
Repo: "repo",
Format: "json",
Args: args,
}
return shortcut.Run(ctx)
}
func findRepoShortcut(t *testing.T, name string) *common.Shortcut {
t.Helper()
for _, shortcut := range Shortcuts() {
if shortcut.Name == name {
return shortcut
}
}
t.Fatalf("shortcut %q not found", name)
return nil
}
func newRepoTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
return httptest.NewServer(handler)
}
func writeJSON(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)
}
}
func assertRequest(t *testing.T, r *http.Request, method, path string) {
t.Helper()
if r.Method != method || r.URL.Path != path {
t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path)
}
}
func assertEqual(t *testing.T, got interface{}, want interface{}) {
t.Helper()
if got != want {
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
}
}

View File

@ -22,6 +22,7 @@ metadata:
|----------|------|----------|
| `repo +list` | 仓库列表 | 否(公开项目) |
| `repo +info` | 仓库详情 | 否(公开项目) |
| `repo +tree` | 仓库文件树 | 否(公开项目) |
| `repo +create` | 创建仓库 | 是 |
| `repo +fork` | Fork 仓库 | 是 |
| `repo +delete` | 删除仓库 | 是 |
@ -39,6 +40,10 @@ gitlink-cli repo +info
# 列出用户的仓库
gitlink-cli repo +list --user zhangsan
# 列出仓库根目录或指定目录
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
# 创建仓库
gitlink-cli repo +create --name my-project --description "项目描述"

View File

@ -0,0 +1,42 @@
# repo +tree
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
列出 GitLink 仓库根目录或指定目录下的文件和子目录。该命令封装 `sub_entries` API适合项目结构检查、文档检查、科研复现性分析和 Agent 自动化报告。
## 命令
```bash
# 列出仓库根目录
gitlink-cli repo +tree --owner someone --repo myrepo
# 指定分支、标签或提交
gitlink-cli repo +tree --owner someone --repo myrepo --ref main
# 列出指定目录
gitlink-cli repo +tree --owner someone --repo myrepo --path src --ref main
# 输出为 JSON
gitlink-cli repo +tree --owner someone --repo myrepo --format json
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--path, -p` | 否 | 要列出的目录路径,默认为仓库根目录 |
| `--ref, -r` | 否 | 分支、标签或提交引用,默认 `master` |
| `--owner` | 否 | 全局参数 - 仓库所有者,可从 git remote 自动解析 |
| `--repo` | 否 | 全局参数 - 仓库名称,可从 git remote 自动解析 |
| `--format` | 否 | 输出格式:`json`/`table`/`yaml` |
| `--debug` | 否 | 启用调试输出 |
## Notes
- GitLink 仓库常见默认分支是 `master`,镜像仓库也可能使用 `main`。如果根目录返回不存在,请显式指定 `--ref main` 或从 `repo +info``default_branch` 字段确认。
- AI Agent 场景建议使用 `--format json`,便于读取 `data.entries` 中的文件名、路径、类型和 SHA。
## References
- [gitlink-repo](../SKILL.md)
- [gitlink-shared](../../gitlink-shared/SKILL.md)