feat(file): 新增 file 快捷命令组(+view/+search/+create/+update/+delete)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
9749a4c832
commit
29e9e927f3
26
README.md
26
README.md
|
|
@ -117,6 +117,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| 📖 Wiki | List, view, create, update, and delete wiki pages |
|
||||
| 🔍 Search | Search repositories, users |
|
||||
| 📊 Dataset | Query research datasets by project |
|
||||
| 📄 File | View, search, create, update, and delete repository files without cloning |
|
||||
| 👤 User | View user profiles and info |
|
||||
| 📊 Profile | User ability, role, major, activity, and contribution statistics |
|
||||
| 📋 PM | Sprint management, kanban boards, weekly reports |
|
||||
|
|
@ -676,6 +677,31 @@ gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --ye
|
|||
> published OpenAPI contract but are not yet deployed on production (they return
|
||||
> 404 there); they will work once the platform enables them.
|
||||
|
||||
### File Operations
|
||||
|
||||
`file` reads and writes repository file contents without cloning — ideal for
|
||||
AI agents that need to read or patch a single file. For directory listings and
|
||||
README viewing, see `repo +tree` and `repo +readme`.
|
||||
|
||||
```bash
|
||||
# View a file (--raw prints only the decoded content, for piping)
|
||||
gitlink-cli file +view --owner Gitlink --repo forgeplus --path README.md
|
||||
gitlink-cli file +view --owner Gitlink --repo forgeplus --path README.md --raw > README.md
|
||||
|
||||
# Search files by name
|
||||
gitlink-cli file +search --owner Gitlink --repo forgeplus --keyword controller
|
||||
|
||||
# Create / update a file (content inline or from a local file)
|
||||
gitlink-cli file +create --owner me --repo proj --path docs/note.md -c "# Note" -b master -m "add note"
|
||||
gitlink-cli file +update --owner me --repo proj --path docs/note.md --content-file note.md -b master
|
||||
|
||||
# Commit to a new branch created from --branch
|
||||
gitlink-cli file +update --owner me --repo proj --path docs/note.md -c "..." -b master --new-branch feature/docs
|
||||
|
||||
# Delete a file
|
||||
gitlink-cli file +delete --owner me --repo proj --path docs/note.md -b master -m "remove note"
|
||||
```
|
||||
|
||||
### Raw API
|
||||
|
||||
For endpoints not covered by shortcuts, use the Raw API directly:
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@
|
|||
| 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
| 📊 数据集 | 按项目查询科研数据集 |
|
||||
| 📄 文件 | 无需克隆即可查看、搜索、创建、更新、删除仓库文件 |
|
||||
| 👤 用户 | 查看用户资料和信息 |
|
||||
| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 |
|
||||
| 📋 项目管理 | Sprint 管理、看板、周报 |
|
||||
|
|
@ -550,6 +551,29 @@ gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --ye
|
|||
```
|
||||
|
||||
> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 遵循已发布的 OpenAPI 契约,但生产环境尚未部署(当前返回 404),待平台上线后即可生效。
|
||||
### 文件操作
|
||||
|
||||
`file` 无需克隆即可读写仓库文件内容,非常适合需要读取或修改单个文件的 AI Agent。目录列表和 README 查看请使用 `repo +tree` 和 `repo +readme`。
|
||||
|
||||
```bash
|
||||
# 查看文件(--raw 仅输出解码后的文件内容,方便管道处理)
|
||||
gitlink-cli file +view --owner Gitlink --repo forgeplus --path README.md
|
||||
gitlink-cli file +view --owner Gitlink --repo forgeplus --path README.md --raw > README.md
|
||||
|
||||
# 按文件名搜索
|
||||
gitlink-cli file +search --owner Gitlink --repo forgeplus --keyword controller
|
||||
|
||||
# 创建 / 更新文件(内容可内联或来自本地文件)
|
||||
gitlink-cli file +create --owner me --repo proj --path docs/note.md -c "# 笔记" -b master -m "add note"
|
||||
gitlink-cli file +update --owner me --repo proj --path docs/note.md --content-file note.md -b master
|
||||
|
||||
# 提交到从 --branch 新建的分支
|
||||
gitlink-cli file +update --owner me --repo proj --path docs/note.md -c "..." -b master --new-branch feature/docs
|
||||
|
||||
# 删除文件
|
||||
gitlink-cli file +delete --owner me --repo proj --path docs/note.md -b master -m "remove note"
|
||||
```
|
||||
|
||||
### Raw API
|
||||
|
||||
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
# File Content Shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
Adds a `file` shortcut group so users and AI agents can read, search, and write
|
||||
repository file contents without cloning or falling back to Raw API calls.
|
||||
Directory listing and README viewing remain covered by `repo +tree` and
|
||||
`repo +readme`.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `gitlink-cli file +view` | View a file's contents; `--raw` prints only the decoded content |
|
||||
| `gitlink-cli file +search` | Search repository files by name |
|
||||
| `gitlink-cli file +create` | Create a file and commit it to a branch |
|
||||
| `gitlink-cli file +update` | Update a file and commit it to a branch |
|
||||
| `gitlink-cli file +delete` | Delete a file and commit the removal to a branch |
|
||||
|
||||
## Validation
|
||||
|
||||
- `file +view` accepts `--ref` (branch, tag, or commit SHA) and `--raw`; `--raw`
|
||||
fails with a clear error when the path is a directory.
|
||||
- Write commands require `--path` and `--branch`; `--message` defaults to
|
||||
`<action> <path>` when omitted.
|
||||
- `file +create` / `file +update` accept exactly one of `--content` or
|
||||
`--content-file`; providing both or neither is rejected before any request.
|
||||
- `--new-branch` commits the change to a new branch created from `--branch`.
|
||||
- File content is transported with `text` encoding (verified against production
|
||||
gitlink.org.cn; the documented `base64` encoding is rejected there).
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests cover endpoint paths, query parameter mapping, request payload
|
||||
construction, content-source validation, default commit messages, `--new-branch`
|
||||
propagation, and raw content extraction from entries/README-shaped responses.
|
||||
|
||||
## 中文说明
|
||||
|
||||
### 变更内容
|
||||
|
||||
- 新增 `file` 快捷命令组:`+view`(查看文件内容,`--raw` 仅输出解码后的正文)、
|
||||
`+search`(按文件名搜索)、`+create` / `+update` / `+delete`(通过
|
||||
contents/batch API 直接提交文件增删改)。
|
||||
- 无需克隆仓库即可读写文件,适合 AI Agent 读取 README、修改单个文件等场景
|
||||
(响应社区 issue:API 是否支持自动读取仓库内文件)。
|
||||
- 内容支持 `--content` 内联或 `--content-file` 从本地文件读取(text 编码,
|
||||
已在生产环境验证,文档中的 base64 编码在生产环境会被拒绝);支持
|
||||
`--new-branch` 提交到新分支。
|
||||
- 更新 README 与 README.zh-CN 的功能表和使用示例。
|
||||
|
||||
### 验证
|
||||
|
||||
- `go test ./...`
|
||||
- `go vet ./...`
|
||||
- `go run . file --help`
|
||||
- `go run . file +view --help`
|
||||
- 在生产 gitlink.org.cn 真实仓库验证 `+view --raw`、`+search`、`+create`、
|
||||
`+update`、`+delete` 全链路
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// Shortcuts returns all file shortcuts.
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
viewShortcut(),
|
||||
searchShortcut(),
|
||||
writeShortcut("create", "Create a new file in the repository"),
|
||||
writeShortcut("update", "Update an existing file in the repository"),
|
||||
deleteShortcut(),
|
||||
}
|
||||
}
|
||||
|
||||
func refFlag() common.Flag {
|
||||
return common.Flag{Name: "ref", Usage: "Branch, tag, or commit SHA (defaults to the default branch)"}
|
||||
}
|
||||
|
||||
func viewShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "view",
|
||||
Description: "View the contents of a file",
|
||||
Flags: []common.Flag{
|
||||
{Name: "path", Short: "p", Usage: "File path", Required: true},
|
||||
refFlag(),
|
||||
{Name: "raw", Usage: "Print only the decoded file content", Bool: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := ctx.RequireArg("path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("filepath", path)
|
||||
if ref := ctx.Arg("ref"); ref != "" {
|
||||
q.Set("ref", ref)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.Arg("raw") == "true" {
|
||||
return printRawContent(env.Data)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func searchShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "search",
|
||||
Description: "Search files in the repository by name",
|
||||
Flags: []common.Flag{
|
||||
{Name: "keyword", Short: "k", Usage: "Search keyword", Required: true},
|
||||
refFlag(),
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
keyword, err := ctx.RequireArg("keyword")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("search", keyword)
|
||||
if ref := ctx.Arg("ref"); ref != "" {
|
||||
q.Set("ref", ref)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func writeShortcut(action, description string) *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: action,
|
||||
Description: description,
|
||||
Flags: []common.Flag{
|
||||
{Name: "path", Short: "p", Usage: "File path", Required: true},
|
||||
{Name: "content", Short: "c", Usage: "File content"},
|
||||
{Name: "content-file", Usage: "Read file content from a local file"},
|
||||
{Name: "branch", Short: "b", Usage: "Branch to commit to", Required: true},
|
||||
{Name: "new-branch", Usage: "Create a new branch from --branch for the commit"},
|
||||
{Name: "message", Short: "m", Usage: "Commit message"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := ctx.RequireArg("path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
branch, err := ctx.RequireArg("branch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := resolveContent(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message := ctx.Arg("message")
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("%s %s", action, path)
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"files": []map[string]interface{}{
|
||||
{
|
||||
"action_type": action,
|
||||
"file_path": path,
|
||||
"content": content,
|
||||
"encoding": "text",
|
||||
},
|
||||
},
|
||||
"branch": branch,
|
||||
"message": message,
|
||||
}
|
||||
if nb := ctx.Arg("new-branch"); nb != "" {
|
||||
payload["new_branch"] = nb
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/contents/batch", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func deleteShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "delete",
|
||||
Description: "Delete a file from the repository",
|
||||
Flags: []common.Flag{
|
||||
{Name: "path", Short: "p", Usage: "File path", Required: true},
|
||||
{Name: "branch", Short: "b", Usage: "Branch to commit to", Required: true},
|
||||
{Name: "new-branch", Usage: "Create a new branch from --branch for the commit"},
|
||||
{Name: "message", Short: "m", Usage: "Commit message"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := ctx.RequireArg("path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
branch, err := ctx.RequireArg("branch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message := ctx.Arg("message")
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("delete %s", path)
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"files": []map[string]interface{}{
|
||||
{
|
||||
"action_type": "delete",
|
||||
"file_path": path,
|
||||
"content": "",
|
||||
"encoding": "text",
|
||||
},
|
||||
},
|
||||
"branch": branch,
|
||||
"message": message,
|
||||
}
|
||||
if nb := ctx.Arg("new-branch"); nb != "" {
|
||||
payload["new_branch"] = nb
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/contents/batch", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// resolveContent reads file content from --content or --content-file.
|
||||
func resolveContent(ctx *common.RuntimeContext) (string, error) {
|
||||
content := ctx.Arg("content")
|
||||
contentFile := ctx.Arg("content-file")
|
||||
if content != "" && contentFile != "" {
|
||||
return "", fmt.Errorf("use only one of --content or --content-file")
|
||||
}
|
||||
if contentFile != "" {
|
||||
data, err := os.ReadFile(contentFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read content file: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
if content == "" {
|
||||
return "", fmt.Errorf("one of --content or --content-file is required")
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// printRawContent extracts and prints the decoded file content from an API
|
||||
// response (entries object, readme object, or a bare content field).
|
||||
func printRawContent(data interface{}) error {
|
||||
content, encoding, ok := extractContent(data)
|
||||
if !ok {
|
||||
return fmt.Errorf("no file content in response (is the path a directory?)")
|
||||
}
|
||||
if encoding == "base64" {
|
||||
if decoded, err := base64.StdEncoding.DecodeString(content); err == nil {
|
||||
fmt.Print(string(decoded))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
fmt.Print(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractContent(data interface{}) (content, encoding string, ok bool) {
|
||||
m, isMap := data.(map[string]interface{})
|
||||
if !isMap {
|
||||
return "", "", false
|
||||
}
|
||||
if entries, has := m["entries"]; has {
|
||||
if em, isEM := entries.(map[string]interface{}); isEM {
|
||||
m = em
|
||||
}
|
||||
}
|
||||
c, has := m["content"].(string)
|
||||
if !has {
|
||||
return "", "", false
|
||||
}
|
||||
if t, hasType := m["type"].(string); hasType && t != "file" {
|
||||
return "", "", false
|
||||
}
|
||||
enc, _ := m["encoding"].(string)
|
||||
return c, enc, true
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestFileView(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/owner/repo/sub_entries.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if got := r.URL.Query().Get("filepath"); got != "README.md" {
|
||||
t.Fatalf("filepath query = %q, want README.md", got)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"entries": map[string]interface{}{
|
||||
"name": "README.md", "type": "file", "content": "# hello",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runFileShortcut(t, server, "view", map[string]string{"path": "README.md"}); err != nil {
|
||||
t.Fatalf("view shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSearch(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/owner/repo/files.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if got := r.URL.Query().Get("search"); got != "main" {
|
||||
t.Fatalf("search query = %q, want main", got)
|
||||
}
|
||||
writeJSON(t, w, []interface{}{})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runFileShortcut(t, server, "search", map[string]string{"keyword": "main"}); err != nil {
|
||||
t.Fatalf("search shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileCreate(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/contents/batch.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&payload)
|
||||
writeJSON(t, w, map[string]interface{}{"commit": map[string]interface{}{"sha": "abc"}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runFileShortcut(t, server, "create", map[string]string{
|
||||
"path": "notes.md", "content": "hello", "branch": "master", "message": "add notes",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create shortcut failed: %v", err)
|
||||
}
|
||||
if payload["branch"] != "master" || payload["message"] != "add notes" {
|
||||
t.Fatalf("payload = %v", payload)
|
||||
}
|
||||
files := payload["files"].([]interface{})
|
||||
f := files[0].(map[string]interface{})
|
||||
if f["action_type"] != "create" || f["file_path"] != "notes.md" || f["encoding"] != "text" {
|
||||
t.Fatalf("file entry = %v", f)
|
||||
}
|
||||
if f["content"] != "hello" {
|
||||
t.Fatalf("content = %v, want hello", f["content"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileUpdateFromContentFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
local := filepath.Join(dir, "input.txt")
|
||||
os.WriteFile(local, []byte("updated"), 0600)
|
||||
|
||||
var payload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/contents/batch.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&payload)
|
||||
writeJSON(t, w, map[string]interface{}{"commit": map[string]interface{}{"sha": "def"}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runFileShortcut(t, server, "update", map[string]string{
|
||||
"path": "notes.md", "content-file": local, "branch": "master", "new-branch": "feature/x",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update shortcut failed: %v", err)
|
||||
}
|
||||
if payload["new_branch"] != "feature/x" {
|
||||
t.Fatalf("new_branch = %v", payload["new_branch"])
|
||||
}
|
||||
f := payload["files"].([]interface{})[0].(map[string]interface{})
|
||||
if f["action_type"] != "update" {
|
||||
t.Fatalf("action_type = %v", f["action_type"])
|
||||
}
|
||||
if f["content"] != "updated" {
|
||||
t.Fatalf("content = %v, want updated", f["content"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileDelete(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/contents/batch.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&payload)
|
||||
writeJSON(t, w, map[string]interface{}{"commit": map[string]interface{}{"sha": "ghi"}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runFileShortcut(t, server, "delete", map[string]string{
|
||||
"path": "notes.md", "branch": "master",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("delete shortcut failed: %v", err)
|
||||
}
|
||||
f := payload["files"].([]interface{})[0].(map[string]interface{})
|
||||
if f["action_type"] != "delete" || f["file_path"] != "notes.md" {
|
||||
t.Fatalf("file entry = %v", f)
|
||||
}
|
||||
if payload["message"] != "delete notes.md" {
|
||||
t.Fatalf("default message = %v", payload["message"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileCreateContentConflicts(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("should not reach server")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runFileShortcut(t, server, "create", map[string]string{
|
||||
"path": "a", "content": "x", "content-file": "y", "branch": "master",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for both --content and --content-file")
|
||||
}
|
||||
|
||||
err = runFileShortcut(t, server, "create", map[string]string{
|
||||
"path": "a", "branch": "master",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no content source is provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data interface{}
|
||||
wantContent string
|
||||
wantEncoding string
|
||||
wantOK bool
|
||||
}{
|
||||
{"entries object", map[string]interface{}{"entries": map[string]interface{}{"type": "file", "content": "abc"}}, "abc", "", true},
|
||||
{"readme object", map[string]interface{}{"type": "file", "content": "abc", "encoding": "base64"}, "abc", "base64", true},
|
||||
{"directory", map[string]interface{}{"type": "dir", "content": "x"}, "", "", false},
|
||||
{"no content", map[string]interface{}{"type": "file"}, "", "", false},
|
||||
{"not a map", []interface{}{}, "", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
content, encoding, ok := extractContent(tt.data)
|
||||
if content != tt.wantContent || encoding != tt.wantEncoding || ok != tt.wantOK {
|
||||
t.Fatalf("extractContent() = (%q, %q, %v), want (%q, %q, %v)",
|
||||
content, encoding, ok, tt.wantContent, tt.wantEncoding, tt.wantOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runFileShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findFileShortcut(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 findFileShortcut(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 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/compare"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/dataset"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/file"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/health"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/ignore"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/issue"
|
||||
|
|
@ -53,6 +54,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"ci": ci.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"dataset": dataset.Shortcuts(tr),
|
||||
"file": file.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(tr),
|
||||
"wiki": wiki.Shortcuts(),
|
||||
"health": health.Shortcuts(tr),
|
||||
|
|
@ -78,6 +80,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"ci": tr.T("cmd.ci.short"),
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"dataset": tr.T("cmd.dataset.short"),
|
||||
"file": "Repository file content operations",
|
||||
"webhook": tr.T("cmd.webhook.short"),
|
||||
"wiki": "Wiki page management",
|
||||
"health": "Project health data collection",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
|
|||
"repo", "issue", "label", "license", "pr", "profile", "release", "branch",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"dataset", "health", "ignore", "wiki",
|
||||
"dataset", "file", "health", "ignore", "wiki",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
Loading…
Reference in New Issue