feat(repo): add raw file content shortcuts
This commit is contained in:
parent
71ca2bb683
commit
1db1b39d05
|
|
@ -227,6 +227,11 @@ gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
|||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
|
||||
|
||||
# Read raw files and common dependency manifests
|
||||
gitlink-cli repo +raw --owner Gitlink --repo forgeplus --path LICENSE --ref master
|
||||
gitlink-cli repo +file-exists --owner Gitlink --repo forgeplus --path package.json --ref master
|
||||
gitlink-cli repo +manifest --owner Gitlink --repo forgeplus --kind go --ref master
|
||||
|
||||
# Show language breakdown
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,11 @@ gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
|||
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 +raw --owner Gitlink --repo forgeplus --path LICENSE --ref master
|
||||
gitlink-cli repo +file-exists --owner Gitlink --repo forgeplus --path package.json --ref master
|
||||
gitlink-cli repo +manifest --owner Gitlink --repo forgeplus --kind go --ref master
|
||||
|
||||
# 查看语言占比
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
# Repository raw file shortcuts
|
||||
|
||||
## Background
|
||||
|
||||
Repository inspection workflows often need to read files such as `LICENSE`,
|
||||
`README.md`, dependency manifests, and source files. Those workflows previously
|
||||
had to use Raw API calls for `/raw/<ref>/<path>` and `sub_entries`.
|
||||
|
||||
This change adds first-class read-only shortcuts for raw file content and common
|
||||
manifest discovery.
|
||||
|
||||
## New shortcuts
|
||||
|
||||
- `repo +raw` reads raw file content at a branch, tag, or commit ref.
|
||||
- `repo +file-exists` checks whether a repository file exists without returning
|
||||
the full content.
|
||||
- `repo +manifest` reads common dependency manifests for `go`, `node`,
|
||||
`python`, `rust`, and `java`.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
gitlink-cli repo +raw --owner Gitlink --repo forgeplus --path LICENSE --ref master
|
||||
gitlink-cli repo +file-exists --owner Gitlink --repo forgeplus --path package.json --ref master
|
||||
gitlink-cli repo +manifest --owner Gitlink --repo forgeplus --kind go --ref master
|
||||
```
|
||||
|
||||
## Manifest mapping
|
||||
|
||||
| Kind | Candidate files |
|
||||
|------|-----------------|
|
||||
| `go` | `go.mod` |
|
||||
| `node` | `package.json` |
|
||||
| `python` | `requirements.txt`, `pyproject.toml`, `Pipfile`, `setup.py` |
|
||||
| `rust` | `Cargo.toml` |
|
||||
| `java` | `pom.xml`, `build.gradle`, `build.gradle.kts` |
|
||||
|
||||
## Safety and path handling
|
||||
|
||||
- These shortcuts are read-only.
|
||||
- `--ref` defaults to `master`.
|
||||
- File paths are cleaned of leading/trailing slashes.
|
||||
- Parent path segments (`..`) are rejected before making API requests.
|
||||
- Raw paths are escaped segment by segment for paths containing spaces or other
|
||||
special characters.
|
||||
|
||||
## Documentation updates
|
||||
|
||||
- README and README.zh-CN include raw file, file existence, and manifest examples.
|
||||
- `skills/gitlink-repo` documents the new shortcuts.
|
||||
- `skills/gitlink-compliance` and `skills/gitlink-code-review` now prefer
|
||||
shortcuts over Raw API calls for file tree and content reads.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests cover:
|
||||
|
||||
- raw file path construction and default refs;
|
||||
- file existence checks through `sub_entries`;
|
||||
- manifest kind mapping and fallback behavior;
|
||||
- missing manifests;
|
||||
- invalid paths and unknown manifest kinds;
|
||||
- HTTP error propagation.
|
||||
|
||||
Suggested verification:
|
||||
|
||||
```bash
|
||||
go test ./shortcuts/repo ./shortcuts
|
||||
```
|
||||
|
||||
Full project verification:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
|
@ -83,9 +83,12 @@
|
|||
"cmd.release.view.short": "View release details",
|
||||
"cmd.repo.create.short": "Create a new repository",
|
||||
"cmd.repo.delete.short": "Delete a repository",
|
||||
"cmd.repo.file_exists.short": "Check whether a repository file exists",
|
||||
"cmd.repo.fork.short": "Fork a repository",
|
||||
"cmd.repo.info.short": "Show repository details",
|
||||
"cmd.repo.list.short": "List repositories for a user or organization",
|
||||
"cmd.repo.manifest.short": "Read a common dependency manifest",
|
||||
"cmd.repo.raw.short": "Read raw repository file content",
|
||||
"cmd.repo.short": "Repository operations",
|
||||
"cmd.repo.tree.short": "List repository files and directories",
|
||||
"cmd.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.",
|
||||
|
|
@ -210,8 +213,12 @@
|
|||
"flag.repo": "Repository name (auto-detected from git remote)",
|
||||
"flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)",
|
||||
"flag.repo.description": "Repository description",
|
||||
"flag.repo.file_exists.path": "File path to check",
|
||||
"flag.repo.manifest.kind": "Manifest kind: go, node, python, rust, or java",
|
||||
"flag.repo.name": "Repository name",
|
||||
"flag.repo.private": "Make repository private (true/false)",
|
||||
"flag.repo.raw.path": "File path in the repository",
|
||||
"flag.repo.raw.ref": "Branch, tag, or commit ref",
|
||||
"flag.repo.tree.path": "Directory path to list (default: repository root)",
|
||||
"flag.repo.tree.ref": "Branch, tag, or commit ref",
|
||||
"flag.search.keyword": "Search keyword",
|
||||
|
|
|
|||
|
|
@ -83,9 +83,12 @@
|
|||
"cmd.release.view.short": "查看发布详情",
|
||||
"cmd.repo.create.short": "创建新仓库",
|
||||
"cmd.repo.delete.short": "删除仓库",
|
||||
"cmd.repo.file_exists.short": "检查仓库文件是否存在",
|
||||
"cmd.repo.fork.short": "Fork 仓库",
|
||||
"cmd.repo.info.short": "显示仓库详情",
|
||||
"cmd.repo.list.short": "列出用户或组织的仓库",
|
||||
"cmd.repo.manifest.short": "读取常见依赖清单文件",
|
||||
"cmd.repo.raw.short": "读取仓库原始文件内容",
|
||||
"cmd.repo.short": "仓库操作",
|
||||
"cmd.repo.tree.short": "列出仓库文件和目录",
|
||||
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
|
||||
|
|
@ -210,8 +213,12 @@
|
|||
"flag.repo": "仓库名称(自动从 git remote 检测)",
|
||||
"flag.repo.category": "筛选:manage/mirror/sync/fork/all(默认:manage)",
|
||||
"flag.repo.description": "仓库描述",
|
||||
"flag.repo.file_exists.path": "要检查的文件路径",
|
||||
"flag.repo.manifest.kind": "清单类型:go、node、python、rust 或 java",
|
||||
"flag.repo.name": "仓库名称",
|
||||
"flag.repo.private": "设为私有仓库(true/false)",
|
||||
"flag.repo.raw.path": "仓库中的文件路径",
|
||||
"flag.repo.raw.ref": "分支、标签或提交引用",
|
||||
"flag.repo.tree.path": "要列出的目录路径(默认:仓库根目录)",
|
||||
"flag.repo.tree.ref": "分支、标签或提交引用",
|
||||
"flag.search.keyword": "搜索关键词",
|
||||
|
|
|
|||
|
|
@ -108,6 +108,33 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "raw",
|
||||
Description: tr.T("cmd.repo.raw.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "path", Short: "p", Usage: tr.T("flag.repo.raw.path"), Required: true},
|
||||
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.raw.ref"), Default: "master"},
|
||||
},
|
||||
Run: runRawFile,
|
||||
},
|
||||
{
|
||||
Name: "file-exists",
|
||||
Description: tr.T("cmd.repo.file_exists.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "path", Short: "p", Usage: tr.T("flag.repo.file_exists.path"), Required: true},
|
||||
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.raw.ref"), Default: "master"},
|
||||
},
|
||||
Run: runFileExists,
|
||||
},
|
||||
{
|
||||
Name: "manifest",
|
||||
Description: tr.T("cmd.repo.manifest.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "kind", Short: "k", Usage: tr.T("flag.repo.manifest.kind"), Required: true},
|
||||
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.raw.ref"), Default: "master"},
|
||||
},
|
||||
Run: runManifest,
|
||||
},
|
||||
{
|
||||
Name: "languages",
|
||||
Description: "Show repository language statistics",
|
||||
|
|
@ -256,6 +283,14 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
}
|
||||
}
|
||||
|
||||
var manifestPathsByKind = map[string][]string{
|
||||
"go": {"go.mod"},
|
||||
"node": {"package.json"},
|
||||
"python": {"requirements.txt", "pyproject.toml", "Pipfile", "setup.py"},
|
||||
"rust": {"Cargo.toml"},
|
||||
"java": {"pom.xml", "build.gradle", "build.gradle.kts"},
|
||||
}
|
||||
|
||||
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
return translators[0]
|
||||
|
|
@ -263,6 +298,79 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
|||
return i18n.Default()
|
||||
}
|
||||
|
||||
func runRawFile(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := requireRepoPathArg(ctx, "path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ref := repoRef(ctx.Arg("ref"))
|
||||
env, err := ctx.CallAPI("GET", rawFilePath(ctx, ref, path), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runFileExists(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := requireRepoPathArg(ctx, "path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ref := repoRef(ctx.Arg("ref"))
|
||||
exists, entry, err := repoFileExists(ctx, path, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"ref": ref,
|
||||
"path": path,
|
||||
"exists": exists,
|
||||
"entry": entry,
|
||||
})
|
||||
}
|
||||
|
||||
func runManifest(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
kind := strings.ToLower(strings.TrimSpace(ctx.Arg("kind")))
|
||||
paths, ok := manifestPathsByKind[kind]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid --kind %q: use go, node, python, rust, or java", ctx.Arg("kind"))
|
||||
}
|
||||
ref := repoRef(ctx.Arg("ref"))
|
||||
checked := make([]string, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
checked = append(checked, path)
|
||||
exists, _, err := repoFileExists(ctx, path, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", rawFilePath(ctx, ref, path), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"ref": ref,
|
||||
"kind": kind,
|
||||
"found": false,
|
||||
"checked": checked,
|
||||
})
|
||||
}
|
||||
|
||||
func runLanguages(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
|
|
@ -482,6 +590,116 @@ func setRepoQueryIfPresent(q url.Values, key, value string) {
|
|||
}
|
||||
}
|
||||
|
||||
func repoRef(ref string) string {
|
||||
if ref := strings.TrimSpace(ref); ref != "" {
|
||||
return ref
|
||||
}
|
||||
return "master"
|
||||
}
|
||||
|
||||
func requireRepoPathArg(ctx *common.RuntimeContext, name string) (string, error) {
|
||||
path, err := ctx.RequireArg(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path = cleanRepoFilePath(path)
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("--%s must not be empty", name)
|
||||
}
|
||||
if hasParentPathSegment(path) {
|
||||
return "", fmt.Errorf("--%s must not contain '..'", name)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func cleanRepoFilePath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
return strings.Trim(path, "/")
|
||||
}
|
||||
|
||||
func rawFilePath(ctx *common.RuntimeContext, ref, path string) string {
|
||||
parts := []string{ctx.RepoPath(), "raw", url.PathEscape(ref)}
|
||||
for _, part := range strings.Split(cleanRepoFilePath(path), "/") {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, url.PathEscape(part))
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
func hasParentPathSegment(path string) bool {
|
||||
for _, part := range strings.Split(cleanRepoFilePath(path), "/") {
|
||||
if part == ".." {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func repoFileExists(ctx *common.RuntimeContext, path, ref string) (bool, map[string]interface{}, error) {
|
||||
q := url.Values{}
|
||||
q.Set("filepath", path)
|
||||
q.Set("ref", ref)
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
entry := findRepoSubEntry(env.Data, path)
|
||||
return entry != nil, entry, nil
|
||||
}
|
||||
|
||||
func findRepoSubEntry(data interface{}, path string) map[string]interface{} {
|
||||
target := lastRepoPathPart(path)
|
||||
if entry, ok := data.(map[string]interface{}); ok {
|
||||
if matchesRepoEntry(entry, target, path) {
|
||||
return entry
|
||||
}
|
||||
for _, key := range []string{"entries", "sub_entries", "files", "data"} {
|
||||
if found := findRepoSubEntryList(entry[key], target, path); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return findRepoSubEntryList(data, target, path)
|
||||
}
|
||||
|
||||
func findRepoSubEntryList(data interface{}, target, path string) map[string]interface{} {
|
||||
entries, ok := data.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, raw := range entries {
|
||||
entry, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if matchesRepoEntry(entry, target, path) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func matchesRepoEntry(entry map[string]interface{}, target, path string) bool {
|
||||
for _, key := range []string{"name", "filename", "path", "filepath"} {
|
||||
value, _ := entry[key].(string)
|
||||
if value == target || cleanRepoFilePath(value) == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func lastRepoPathPart(path string) string {
|
||||
parts := strings.Split(cleanRepoFilePath(path), "/")
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
func parseOptionalRepoNonNegativeInt(value, name string) (int, bool, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return 0, false, nil
|
||||
|
|
|
|||
|
|
@ -218,6 +218,143 @@ func TestRepoTreeShortcutRegistersHelpFlags(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRepoRawUsesRefAndEscapedPath(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
t.Fatalf("expected GET, got %s", r.Method)
|
||||
}
|
||||
assertEqual(t, r.URL.EscapedPath(), "/owner/repo/raw/release%2Fv1/docs/My%20File.md")
|
||||
writeText(t, w, http.StatusOK, "# title\n")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "raw", map[string]string{
|
||||
"ref": "release/v1",
|
||||
"path": "/docs/My File.md",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("raw shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoRawDefaultsRefToMaster(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/raw/master/LICENSE")
|
||||
writeText(t, w, http.StatusOK, "MIT License\n")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "raw", map[string]string{"path": "LICENSE"}); err != nil {
|
||||
t.Fatalf("raw shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFileExistsFindsEntryByName(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/sub_entries.json")
|
||||
assertEqual(t, r.URL.Query().Get("filepath"), "src/main.go")
|
||||
assertEqual(t, r.URL.Query().Get("ref"), "main")
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"entries": []interface{}{
|
||||
map[string]interface{}{"name": "main.go", "type": "file"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "file-exists", map[string]string{
|
||||
"path": "src/main.go",
|
||||
"ref": "main",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("file-exists shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFileExistsReportsMissing(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/sub_entries.json")
|
||||
writeJSON(t, w, map[string]interface{}{"entries": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "file-exists", map[string]string{"path": "missing.txt"}); err != nil {
|
||||
t.Fatalf("file-exists missing shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoManifestGoReadsGoMod(t *testing.T) {
|
||||
requests := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
switch requests {
|
||||
case 1:
|
||||
assertRequest(t, r, "GET", "/owner/repo/sub_entries.json")
|
||||
assertEqual(t, r.URL.Query().Get("filepath"), "go.mod")
|
||||
assertEqual(t, r.URL.Query().Get("ref"), "master")
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"entries": []interface{}{map[string]interface{}{"name": "go.mod", "type": "file"}},
|
||||
})
|
||||
case 2:
|
||||
assertRequest(t, r, "GET", "/owner/repo/raw/master/go.mod")
|
||||
writeText(t, w, http.StatusOK, "module example.com/repo\n")
|
||||
default:
|
||||
t.Fatalf("unexpected extra request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "manifest", map[string]string{"kind": "go"}); err != nil {
|
||||
t.Fatalf("manifest go shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, requests, 2)
|
||||
}
|
||||
|
||||
func TestRepoManifestPythonFallsBackToPyproject(t *testing.T) {
|
||||
requests := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
switch requests {
|
||||
case 1:
|
||||
assertRequest(t, r, "GET", "/owner/repo/sub_entries.json")
|
||||
assertEqual(t, r.URL.Query().Get("filepath"), "requirements.txt")
|
||||
writeJSON(t, w, map[string]interface{}{"entries": []interface{}{}})
|
||||
case 2:
|
||||
assertRequest(t, r, "GET", "/owner/repo/sub_entries.json")
|
||||
assertEqual(t, r.URL.Query().Get("filepath"), "pyproject.toml")
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"entries": []interface{}{map[string]interface{}{"path": "pyproject.toml", "type": "file"}},
|
||||
})
|
||||
case 3:
|
||||
assertRequest(t, r, "GET", "/owner/repo/raw/master/pyproject.toml")
|
||||
writeText(t, w, http.StatusOK, "[project]\n")
|
||||
default:
|
||||
t.Fatalf("unexpected extra request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "manifest", map[string]string{"kind": "python"}); err != nil {
|
||||
t.Fatalf("manifest python shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, requests, 3)
|
||||
}
|
||||
|
||||
func TestRepoManifestReportsNotFound(t *testing.T) {
|
||||
requests := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
assertRequest(t, r, "GET", "/owner/repo/sub_entries.json")
|
||||
writeJSON(t, w, map[string]interface{}{"entries": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "manifest", map[string]string{"kind": "java"}); err != nil {
|
||||
t.Fatalf("manifest missing shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, requests, 3)
|
||||
}
|
||||
|
||||
func TestRepoLanguagesUsesLanguagesEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/languages.json")
|
||||
|
|
@ -526,6 +663,21 @@ func TestRepoInsightValidation(t *testing.T) {
|
|||
shortcut: "unlike",
|
||||
args: map[string]string{"project-id": "-1"},
|
||||
},
|
||||
{
|
||||
name: "raw parent path",
|
||||
shortcut: "raw",
|
||||
args: map[string]string{"path": "../LICENSE"},
|
||||
},
|
||||
{
|
||||
name: "file exists parent path",
|
||||
shortcut: "file-exists",
|
||||
args: map[string]string{"path": "src/../LICENSE"},
|
||||
},
|
||||
{
|
||||
name: "unknown manifest kind",
|
||||
shortcut: "manifest",
|
||||
args: map[string]string{"kind": "ruby"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
|
@ -573,6 +725,30 @@ func TestRepoInfoHTTPError(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRepoRawHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "raw", map[string]string{"path": "README.md"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFileExistsHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "file-exists", map[string]string{"path": "README.md"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoForkHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
||||
|
|
|
|||
|
|
@ -164,14 +164,14 @@ gitlink-cli api POST /:owner/:repo/pulls/:id/reviews --body '{
|
|||
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
|
||||
|
||||
# 2. 获取仓库文件列表(遍历关键目录)
|
||||
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=src&ref=master'
|
||||
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=tests&ref=master'
|
||||
gitlink-cli repo +tree --owner <owner> --repo <repo> --path src --ref master --format json
|
||||
gitlink-cli repo +tree --owner <owner> --repo <repo> --path tests --ref master --format json
|
||||
|
||||
# 3. 获取关键文件内容
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/README.md
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/.gitignore
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/.eslintrc.js # 或类似配置
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/package.json # 或 go.mod, Cargo.toml
|
||||
gitlink-cli repo +raw --owner <owner> --repo <repo> --path README.md --ref master --format json
|
||||
gitlink-cli repo +raw --owner <owner> --repo <repo> --path .gitignore --ref master --format json
|
||||
gitlink-cli repo +raw --owner <owner> --repo <repo> --path .eslintrc.js --ref master --format json
|
||||
gitlink-cli repo +manifest --owner <owner> --repo <repo> --kind node --ref master --format json
|
||||
|
||||
# 4. 获取语言统计和贡献者
|
||||
gitlink-cli api GET /:owner/:repo/languages
|
||||
|
|
@ -273,7 +273,7 @@ gitlink-cli api GET /:owner/:repo/pulls/:id/diff --format json
|
|||
gitlink-cli api POST /:owner/:repo/pulls/:id/reviews --body '{"body":"...","event":"COMMENT"}'
|
||||
|
||||
# 获取仓库文件列表
|
||||
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=<path>&ref=<branch>'
|
||||
gitlink-cli repo +tree --owner <owner> --repo <repo> --path <path> --ref <branch> --format json
|
||||
|
||||
# 获取仓库语言统计
|
||||
gitlink-cli api GET /:owner/:repo/languages --format json
|
||||
|
|
|
|||
|
|
@ -39,10 +39,10 @@ metadata:
|
|||
|
||||
```bash
|
||||
# 1. 获取仓库文件结构
|
||||
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=&ref=master'
|
||||
gitlink-cli repo +tree --owner <owner> --repo <repo> --ref master --format json
|
||||
|
||||
# 2. 读取 LICENSE 文件
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/LICENSE
|
||||
gitlink-cli repo +raw --owner <owner> --repo <repo> --path LICENSE --ref master --format json
|
||||
|
||||
# 3. 检查关键文档是否存在
|
||||
# 检查以下文件是否存在:
|
||||
|
|
@ -54,14 +54,14 @@ gitlink-cli api GET /:owner/:repo/raw/master/LICENSE
|
|||
# - README.md
|
||||
|
||||
# 4. 获取依赖配置
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/package.json # Node.js
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/go.mod # Go
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/requirements.txt # Python
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/Cargo.toml # Rust
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/pom.xml # Java/Maven
|
||||
gitlink-cli repo +manifest --owner <owner> --repo <repo> --kind node --ref master --format json
|
||||
gitlink-cli repo +manifest --owner <owner> --repo <repo> --kind go --ref master --format json
|
||||
gitlink-cli repo +manifest --owner <owner> --repo <repo> --kind python --ref master --format json
|
||||
gitlink-cli repo +manifest --owner <owner> --repo <repo> --kind rust --ref master --format json
|
||||
gitlink-cli repo +manifest --owner <owner> --repo <repo> --kind java --ref master --format json
|
||||
|
||||
# 5. 获取源文件检查(按语言采样)
|
||||
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=src&ref=master'
|
||||
gitlink-cli repo +tree --owner <owner> --repo <repo> --path src --ref master --format json
|
||||
|
||||
# 6. 获取仓库基本信息
|
||||
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
|
||||
|
|
@ -146,7 +146,7 @@ gitlink-cli repo +info --owner <owner> --repo <repo> --format json
|
|||
|
||||
```bash
|
||||
# 1. 获取依赖配置文件
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/package.json
|
||||
gitlink-cli repo +manifest --owner <owner> --repo <repo> --kind node --ref master --format json
|
||||
```
|
||||
|
||||
### 许可证兼容性参考
|
||||
|
|
@ -181,10 +181,10 @@ gitlink-cli api GET /:owner/:repo/raw/master/package.json
|
|||
|
||||
```bash
|
||||
# 1. 遍历源文件目录
|
||||
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=src&ref=master'
|
||||
gitlink-cli repo +tree --owner <owner> --repo <repo> --path src --ref master --format json
|
||||
|
||||
# 2. 采样检查源文件头部(取前 5-10 行)
|
||||
gitlink-cli api GET /:owner/:repo/raw/master/src/main.py
|
||||
gitlink-cli repo +raw --owner <owner> --repo <repo> --path src/main.py --ref master --format json
|
||||
```
|
||||
|
||||
### 标准版权声明模板
|
||||
|
|
@ -210,10 +210,10 @@ gitlink-cli api GET /:owner/:repo/raw/master/src/main.py
|
|||
|
||||
```bash
|
||||
# 获取文件内容
|
||||
gitlink-cli api GET /:owner/:repo/raw/<branch>/<path>
|
||||
gitlink-cli repo +raw --owner <owner> --repo <repo> --path <path> --ref <branch> --format json
|
||||
|
||||
# 获取文件列表(遍历目录)
|
||||
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=<path>&ref=<branch>'
|
||||
gitlink-cli repo +tree --owner <owner> --repo <repo> --path <path> --ref <branch> --format json
|
||||
|
||||
# 获取仓库信息
|
||||
gitlink-cli api GET /:owner/:repo --format json
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-repo
|
||||
version: 1.0.0
|
||||
description: "仓库管理:创建、查看、Fork、删除仓库,查看 README、语言统计、贡献者、关注者,并执行关注/点赞等互动操作。当用户需要操作或分析 GitLink 仓库时触发。"
|
||||
version: 2.0.0
|
||||
description: "仓库管理:创建、查看、Fork、删除仓库,查看 README、文件树、原始文件、依赖清单、语言统计、贡献者、关注者,并执行关注/点赞等互动操作。当用户需要操作或分析 GitLink 仓库时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -24,6 +24,9 @@ metadata:
|
|||
| `repo +info` | 仓库详情 | 否(公开项目) |
|
||||
| `repo +readme` | README 内容 | 否(公开项目) |
|
||||
| `repo +tree` | 仓库文件树 | 否(公开项目) |
|
||||
| `repo +raw` | 读取原始文件内容 | 否(公开项目) |
|
||||
| `repo +file-exists` | 检查文件是否存在 | 否(公开项目) |
|
||||
| `repo +manifest` | 读取常见依赖清单 | 否(公开项目) |
|
||||
| `repo +languages` | 仓库语言统计 | 否(公开项目) |
|
||||
| `repo +contributors` | 仓库贡献者列表 | 否(公开项目) |
|
||||
| `repo +contributor-stats` | 贡献者代码行统计 | 否(公开项目) |
|
||||
|
|
@ -54,6 +57,9 @@ 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 +raw --owner Gitlink --repo forgeplus --path LICENSE --ref master
|
||||
gitlink-cli repo +file-exists --owner Gitlink --repo forgeplus --path package.json --ref master
|
||||
gitlink-cli repo +manifest --owner Gitlink --repo forgeplus --kind go --ref master
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
|
||||
|
||||
|
|
@ -93,11 +99,11 @@ gitlink-cli api GET /:owner/:repo/commits --query 'page=1&limit=20'
|
|||
# 获取标签列表
|
||||
gitlink-cli api GET /:owner/:repo/tags
|
||||
|
||||
# 获取文件内容
|
||||
gitlink-cli api GET /:owner/:repo/raw/main/README.md
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `repo +delete` 是不可逆操作,执行前必须确认用户意图
|
||||
- 创建仓库默认为公开,使用 `--private true` 创建私有仓库
|
||||
- `repo +raw` 会读取指定 `--ref` 下的文件内容,默认 `master`
|
||||
- `repo +manifest --kind` 支持 `go`、`node`、`python`、`rust`、`java`
|
||||
|
|
|
|||
Loading…
Reference in New Issue