Merge PR #273: feat(repo): 补齐仓库治理中的转移与模块配置能力

# Conflicts:
#	shortcuts/repo/repo.go
#	skills/README.md
#	skills/gitlink-repo/SKILL.md
This commit is contained in:
wbtiger 2026-07-14 22:50:02 +08:00
commit 0b8aa5ff7f
8 changed files with 401 additions and 330 deletions

View File

@ -249,11 +249,30 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
gitlink-cli repo +like --owner Gitlink --repo forgeplus
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
# List and update repository navigation units
gitlink-cli repo +units --owner Gitlink --repo forgeplus
gitlink-cli repo +set-units --owner Gitlink --repo forgeplus --units code,issues,pulls,wiki
# Create a repository
gitlink-cli repo +create -n my-project -d "Project description"
# Fork a repository
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
# List organizations that can receive a repository transfer
gitlink-cli repo +transfer-orgs --owner Gitlink --repo forgeplus
# Preview a repository transfer without changing data
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --dry-run
# Confirm and send a repository transfer request
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --yes
# Preview canceling a pending repository transfer
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --dry-run
# Confirm canceling a pending repository transfer
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --yes
```
### Webhook Management

View File

@ -260,11 +260,30 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
gitlink-cli repo +like --owner Gitlink --repo forgeplus
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
# 查看和更新仓库导航模块
gitlink-cli repo +units --owner Gitlink --repo forgeplus
gitlink-cli repo +set-units --owner Gitlink --repo forgeplus --units code,issues,pulls,wiki
# 创建仓库
gitlink-cli repo +create -n my-project -d "项目描述"
# Fork 仓库
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
# 列出可接收仓库转移的组织
gitlink-cli repo +transfer-orgs --owner Gitlink --repo forgeplus
# 预览仓库转移请求,不修改线上数据
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --dry-run
# 确认后发起仓库转移
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --yes
# 预览取消待处理的仓库转移
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --dry-run
# 确认后取消待处理的仓库转移
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --yes
```
### Webhook 管理

View File

@ -88,8 +88,13 @@
"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.set_units.short": "Set enabled repository navigation units",
"cmd.repo.short": "Repository operations",
"cmd.repo.transfer.short": "Transfer a repository to another owner",
"cmd.repo.transfer_cancel.short": "Cancel a pending repository transfer",
"cmd.repo.transfer_orgs.short": "List organizations that can receive this repository",
"cmd.repo.tree.short": "List repository files and directories",
"cmd.repo.units.short": "List enabled repository navigation units",
"cmd.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.",
"cmd.root.short": "GitLink CLI - command-line tool for GitLink",
"cmd.search.repos.short": "Search repositories",
@ -215,8 +220,13 @@
"flag.repo.description": "Repository description",
"flag.repo.name": "Repository name",
"flag.repo.private": "Make repository private (true/false)",
"flag.repo.target_owner": "Target user or organization login",
"flag.repo.transfer_cancel_dry_run": "Preview the cancel request without changing repository transfer state",
"flag.repo.transfer_dry_run": "Preview the transfer request without changing repository ownership",
"flag.repo.transfer_yes": "Confirm and execute the repository transfer action",
"flag.repo.tree.path": "Directory path to list (default: repository root)",
"flag.repo.tree.ref": "Branch, tag, or commit ref",
"flag.repo.units": "Comma-separated units: code,issues,pulls,devops,versions,wiki,services,resources",
"flag.search.keyword": "Search keyword",
"flag.sort_by": "Sort field",
"flag.sort_direction": "Sort direction: asc, desc",

View File

@ -88,8 +88,13 @@
"cmd.repo.fork.short": "Fork 仓库",
"cmd.repo.info.short": "显示仓库详情",
"cmd.repo.list.short": "列出用户或组织的仓库",
"cmd.repo.set_units.short": "设置启用的仓库导航模块",
"cmd.repo.short": "仓库操作",
"cmd.repo.transfer.short": "将仓库转移给其他所有者",
"cmd.repo.transfer_cancel.short": "取消待处理的仓库转移",
"cmd.repo.transfer_orgs.short": "列出可接收该仓库的组织",
"cmd.repo.tree.short": "列出仓库文件和目录",
"cmd.repo.units.short": "列出启用的仓库导航模块",
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
"cmd.root.short": "GitLink CLI - GitLink 命令行工具",
"cmd.search.repos.short": "搜索仓库",
@ -215,8 +220,13 @@
"flag.repo.description": "仓库描述",
"flag.repo.name": "仓库名称",
"flag.repo.private": "设为私有仓库true/false",
"flag.repo.target_owner": "目标用户或组织登录名",
"flag.repo.transfer_cancel_dry_run": "预览取消请求,不修改仓库转移状态",
"flag.repo.transfer_dry_run": "预览转移请求,不修改仓库所有者",
"flag.repo.transfer_yes": "确认并执行仓库转移相关操作",
"flag.repo.tree.path": "要列出的目录路径(默认:仓库根目录)",
"flag.repo.tree.ref": "分支、标签或提交引用",
"flag.repo.units": "逗号分隔的模块code,issues,pulls,devops,versions,wiki,services,resources",
"flag.search.keyword": "搜索关键词",
"flag.sort_by": "排序字段",
"flag.sort_direction": "排序方向asc、desc",

View File

@ -81,6 +81,41 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "units",
Description: tr.T("cmd.repo.units.short"),
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", repoUnitsPath(ctx), nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "set-units",
Description: tr.T("cmd.repo.set_units.short"),
Flags: []common.Flag{
{Name: "units", Short: "u", Usage: tr.T("flag.repo.units"), Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
units, err := parseRepoUnits(ctx.Arg("units"))
if err != nil {
return err
}
env, err := ctx.CallAPI("POST", repoUnitsPath(ctx), map[string]interface{}{"unit_types": units})
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "tree",
Description: tr.T("cmd.repo.tree.short"),
@ -239,6 +274,89 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "transfer-orgs",
Description: tr.T("cmd.repo.transfer_orgs.short"),
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", repoTransferPath(ctx, "organizations"), nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "transfer",
Description: tr.T("cmd.repo.transfer.short"),
Flags: []common.Flag{
{Name: "target-owner", Usage: tr.T("flag.repo.target_owner"), Required: true},
{Name: "dry-run", Usage: tr.T("flag.repo.transfer_dry_run"), Bool: true, Default: "false"},
{Name: "yes", Usage: tr.T("flag.repo.transfer_yes"), Bool: true, Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
targetOwner, err := ctx.RequireArg("target-owner")
if err != nil {
return err
}
targetOwner = strings.TrimSpace(targetOwner)
if targetOwner == "" {
return fmt.Errorf("required flag --target-owner is missing")
}
payload := map[string]interface{}{"owner_name": targetOwner}
path := repoTransferPath(ctx, "")
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"method": "POST",
"path": path,
"payload": payload,
})
}
if err := requireRepoTransferConfirmation(ctx, "transfer"); err != nil {
return err
}
env, err := ctx.CallAPI("POST", path, payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "transfer-cancel",
Description: tr.T("cmd.repo.transfer_cancel.short"),
Flags: []common.Flag{
{Name: "dry-run", Usage: tr.T("flag.repo.transfer_cancel_dry_run"), Bool: true, Default: "false"},
{Name: "yes", Usage: tr.T("flag.repo.transfer_yes"), Bool: true, Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
path := repoTransferPath(ctx, "cancel")
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"method": "POST",
"path": path,
})
}
if err := requireRepoTransferConfirmation(ctx, "transfer-cancel"); err != nil {
return err
}
env, err := ctx.CallAPI("POST", path, nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "delete",
Description: tr.T("cmd.repo.delete.short"),
@ -253,277 +371,24 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
// --- Repository Settings Shortcuts ---
{
Name: "detail",
Description: "Show repository full details",
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/detail", nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "simple",
Description: "Show repository simple details",
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/simple", nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "settings",
Description: "Show repository settings",
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/edit", nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "units",
Description: "Show repository navigation units",
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/project_units", nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "units-update",
Description: "Update repository navigation units",
Flags: []common.Flag{
{Name: "units", Usage: "Comma-separated list of unit types (code,issues,pulls,wiki,devops,versions,services)", Required: true},
{Name: "dry-run", Usage: "Preview the action without changing repository state", Bool: true, Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
unitsRaw, err := ctx.RequireArg("units")
if err != nil {
return err
}
unitTypes := parseCommaSeparatedList(unitsRaw)
if len(unitTypes) == 0 {
return fmt.Errorf("--units must contain at least one unit type")
}
validUnits := map[string]bool{
"code": true, "issues": true, "pulls": true, "wiki": true,
"devops": true, "versions": true, "services": true,
}
for _, u := range unitTypes {
if !validUnits[u] {
return fmt.Errorf("invalid unit type %q: must be one of code, issues, pulls, wiki, devops, versions, services", u)
}
}
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "units-update",
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
"unit_types": unitTypes,
})
}
body := map[string]interface{}{
"unit_types": unitTypes,
}
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/project_units", body)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "topics",
Description: "List project topics",
Flags: []common.Flag{
{Name: "keyword", Short: "k", Usage: "Filter topics by keyword"},
},
Run: func(ctx *common.RuntimeContext) error {
q := url.Values{}
if keyword := ctx.Arg("keyword"); keyword != "" {
q.Set("keyword", keyword)
}
env, err := ctx.CallAPIWithQuery("GET", "/v1/project_topics", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "topic-add",
Description: "Add a topic to the repository",
Flags: []common.Flag{
{Name: "name", Short: "n", Usage: "Topic name", Required: true},
{Name: "dry-run", Usage: "Preview the action 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
}
projectID, err := resolveRepoProjectID(ctx)
if err != nil {
return err
}
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "topic-add",
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
"project_id": projectID,
"name": name,
})
}
body := map[string]interface{}{
"name": name,
"project_id": projectID,
}
env, err := ctx.CallAPI("POST", "/v1/project_topics", body)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "topic-delete",
Description: "Delete a topic from the repository",
Flags: []common.Flag{
{Name: "id", Usage: "Topic ID", Required: true},
{Name: "dry-run", Usage: "Preview the action without changing repository state", Bool: true, Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
topicID, err := ctx.RequireArg("id")
if err != nil {
return err
}
projectID, err := resolveRepoProjectID(ctx)
if err != nil {
return err
}
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "topic-delete",
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
"project_id": projectID,
"topic_id": topicID,
})
}
q := url.Values{}
q.Set("project_id", projectID)
env, err := ctx.CallAPIWithQuery("DELETE", fmt.Sprintf("/v1/project_topics/%s", topicID), q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "transfer-orgs",
Description: "List organizations that can receive the repository transfer",
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/applied_transfer_projects/organizations", nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "transfer",
Description: "Transfer repository to another owner",
Flags: []common.Flag{
{Name: "owner", Short: "o", Usage: "Target owner name (user or organization)", Required: true},
{Name: "dry-run", Usage: "Preview the action without changing repository state", Bool: true, Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
ownerName, err := ctx.RequireArg("owner")
if err != nil {
return err
}
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "transfer",
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
"target_owner": ownerName,
})
}
body := map[string]interface{}{
"owner_name": ownerName,
}
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/applied_transfer_projects", body)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "transfer-cancel",
Description: "Cancel pending repository transfer",
Flags: []common.Flag{
{Name: "dry-run", Usage: "Preview the action without changing repository state", Bool: true, Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "transfer-cancel",
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
})
}
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/applied_transfer_projects/cancel", nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
}
}
func repoTransferPath(ctx *common.RuntimeContext, action string) string {
base := ctx.RepoPath() + "/applied_transfer_projects"
if action == "" {
return base
}
return fmt.Sprintf("%s/%s", base, action)
}
func requireRepoTransferConfirmation(ctx *common.RuntimeContext, shortcut string) error {
if ctx.Arg("yes") == "true" {
return nil
}
return fmt.Errorf("refusing to run repo +%s without --yes; use --dry-run to preview the request first", shortcut)
}
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
if len(translators) > 0 && translators[0] != nil {
return translators[0]
@ -769,14 +634,40 @@ func parseRepoPositiveInt(value, name string) (int, error) {
return parsed, nil
}
func parseCommaSeparatedList(s string) []string {
parts := strings.Split(s, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
return result
func repoUnitsPath(ctx *common.RuntimeContext) string {
return ctx.RepoPath() + "/project_units"
}
func parseRepoUnits(raw string) ([]string, error) {
allowed := map[string]bool{
"code": true,
"issues": true,
"pulls": true,
"devops": true,
"versions": true,
"wiki": true,
"services": true,
"resources": true,
}
seen := map[string]bool{}
units := []string{}
for _, part := range strings.Split(raw, ",") {
unit := strings.ToLower(strings.TrimSpace(part))
if unit == "" {
continue
}
if !allowed[unit] {
return nil, fmt.Errorf("invalid repository unit %q; allowed values: code,issues,pulls,devops,versions,wiki,services,resources", unit)
}
if seen[unit] {
continue
}
seen[unit] = true
units = append(units, unit)
}
if len(units) == 0 {
return nil, fmt.Errorf("at least one repository unit is required")
}
return units, nil
}

View File

@ -493,6 +493,129 @@ func TestRepoDelete(t *testing.T) {
}
}
func TestRepoTransferOrgs(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/applied_transfer_projects/organizations.json")
writeJSON(t, w, map[string]interface{}{
"total_count": float64(1),
"organizations": []interface{}{map[string]interface{}{"name": "target-org"}},
})
}))
defer server.Close()
if err := runShortcut(t, server, "transfer-orgs", nil); err != nil {
t.Fatalf("transfer-orgs failed: %v", err)
}
}
func TestRepoTransfer(t *testing.T) {
var body map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/owner/repo/applied_transfer_projects.json")
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode body: %v", err)
}
writeJSON(t, w, map[string]interface{}{"status": "common"})
}))
defer server.Close()
err := runShortcut(t, server, "transfer", map[string]string{
"target-owner": " target-org ",
"yes": "true",
})
if err != nil {
t.Fatalf("transfer failed: %v", err)
}
assertEqual(t, body["owner_name"], "target-org")
}
func TestRepoTransferDryRunDoesNotCallAPI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("dry-run should not call API, got %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runShortcut(t, server, "transfer", map[string]string{
"target-owner": "target-org",
"dry-run": "true",
})
if err != nil {
t.Fatalf("transfer dry-run failed: %v", err)
}
}
func TestRepoTransferRequiresExplicitYes(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("transfer without --yes should not call API, got %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runShortcut(t, server, "transfer", map[string]string{"target-owner": "target-org"})
if err == nil {
t.Fatal("expected confirmation error for missing --yes")
}
}
func TestRepoTransferFailsWithoutTargetOwner(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call should be made")
}))
defer server.Close()
err := runShortcut(t, server, "transfer", map[string]string{})
if err == nil {
t.Fatal("expected error for missing target owner")
}
}
func TestRepoTransferRejectsBlankTargetOwner(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call should be made")
}))
defer server.Close()
err := runShortcut(t, server, "transfer", map[string]string{"target-owner": " "})
if err == nil {
t.Fatal("expected error for blank target owner")
}
}
func TestRepoTransferCancel(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/owner/repo/applied_transfer_projects/cancel.json")
writeJSON(t, w, map[string]interface{}{"status": "canceled"})
}))
defer server.Close()
if err := runShortcut(t, server, "transfer-cancel", map[string]string{"yes": "true"}); err != nil {
t.Fatalf("transfer-cancel failed: %v", err)
}
}
func TestRepoTransferCancelDryRunDoesNotCallAPI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("dry-run should not call API, got %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runShortcut(t, server, "transfer-cancel", map[string]string{"dry-run": "true"})
if err != nil {
t.Fatalf("transfer-cancel dry-run failed: %v", err)
}
}
func TestRepoTransferCancelRequiresExplicitYes(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("transfer-cancel without --yes should not call API, got %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runShortcut(t, server, "transfer-cancel", nil)
if err == nil {
t.Fatal("expected confirmation error for missing --yes")
}
}
func TestRepoCreate(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {

View File

@ -64,41 +64,58 @@ skills/
├── README.md # 本文件
├── gitlink-shared/ # 共享基础规则
│ ├── SKILL.md # 认证、全局参数、安全规则、分支约定
│ └── references/ # API 参考、错误处理
│ └── references/
│ ├── api-reference.md # API 详细参考、错误处理
│ ├── raw-api-batch.md # 批量 Raw API 调用参考
│ └── troubleshooting.md # 常见问题排查
├── gitlink-repo/ # 仓库管理
│ ├── SKILL.md # 仓库操作指南
│ └── references/ # 仓库 API 参考
│ └── references/ # 仓库命令参考文档
├── gitlink-issue/ # Issue 管理
│ ├── SKILL.md # Issue 操作指南
│ └── references/ # Issue API 参考
│ └── references/ # Issue 命令参考文档
├── gitlink-pr/ # Pull Request
│ ├── SKILL.md # PR 操作指南
│ └── references/ # PR API 参考
│ └── references/ # PR 命令参考文档
├── gitlink-member/ # 仓库成员管理
│ └── SKILL.md # 成员与邀请链接操作指南
├── gitlink-branch/ # 分支管理
│ ├── SKILL.md # 分支操作指南
│ └── examples/
│ └── branch-workflow.md # 分支工作流
├── gitlink-release/ # 版本发布
│ ├── SKILL.md # Release 操作指南
│ └── references/ # Release API 参考
│ └── references/ # Release 命令参考文档
├── gitlink-release-auto/ # 自动化 Release 管理
│ └── SKILL.md # 自动发版、版本号推荐、Release Notes 生成
├── gitlink-search/ # 搜索功能
│ ├── SKILL.md # 搜索操作指南
│ └── references/ # 搜索参考
│ └── references/ # 搜索命令参考文档
├── gitlink-user/ # 用户管理
│ └── SKILL.md # 用户操作指南
│ ├── SKILL.md # 用户操作指南
│ └── references/ # 用户命令参考文档
├── gitlink-org/ # 组织管理
│ ├── SKILL.md # 组织操作指南
│ └── references/ # 组织参考
│ └── references/ # 组织命令参考文档
├── gitlink-ci/ # CI/CD
│ └── SKILL.md # CI 操作指南
├── gitlink-pipeline/ # 流水线工作流
│ └── SKILL.md # Pipeline 操作指南
├── gitlink-wiki/ # Wiki 页面管理
│ └── SKILL.md # Wiki 操作指南
├── gitlink-pm/ # 项目管理
│ └── SKILL.md # PM 操作指南
├── gitlink-workflow/ # AI 自动化工作流
│ └── SKILL.md # 工作流模板Issue 分类、PR Review、Release Notes
└── gitlink-maintainer-copilot/ # 维护者驾驶舱
├── SKILL.md # 证据包、治理剧本、写入确认
├── references/ # 证据包、剧本、治理 Issue 模板
└── examples/ # 演示流程和样例报告
├── gitlink-health/ # 项目健康度分析
│ ├── SKILL.md # 健康度分析指南
│ ├── data/
│ │ ├── .gitignore # 忽略 *.db 文件
│ │ └── .gitkeep # 占位文件
│ ├── references/
│ │ └── queries.md # SQL 查询参考
│ └── asset/
│ └── health_report_template.md # 报告模板
└── gitlink-workflow/ # AI 自动化工作流
└── SKILL.md # 工作流模板Issue 分类、PR Review、Release Notes
```
---
@ -110,11 +127,12 @@ skills/
| Skill | 说明 | 常用命令 |
|-------|------|----------|
| **gitlink-shared** | 认证、全局参数、API 参考、安全规则、分支约定 | `auth login`, `auth status` |
| **gitlink-repo** | 仓库管理 | `repo +list`, `repo +create`, `repo +info`, `repo +fork` |
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` |
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +review` |
| **gitlink-repo** | 仓库管理与洞察 | `repo +list`, `repo +info`, `repo +languages`, `repo +contributors`, `repo +code-stats`, `repo +follow`, `repo +like`, `repo +units`, `repo +set-units` |
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close`, `issue +batch-update`, `issue +batch-delete` |
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +versions`, `pr +version-diff`, `pr +reviews`, `pr +review` |
| **gitlink-member** | 仓库成员管理 | `member +list`, `member +add`, `member +batch-add`, `member +role`, `member +invite-link` |
| **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` |
| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +view` |
| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +edit`, `release +update`, `release +view` |
### 辅助 Skills
@ -124,9 +142,11 @@ skills/
| **gitlink-user** | 用户管理 | `user +me`, `user +info` |
| **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` |
| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` |
| **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` |
| **gitlink-wiki** | Wiki 页面管理 | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` |
| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 |
| **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
| **gitlink-maintainer-copilot** | 维护者驾驶舱 | 证据包、治理剧本、确认后创建治理 Issue |
| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md |
---
@ -152,13 +172,13 @@ gitlink-cli repo +info --owner wbtiger --repo gitlink-cli
gitlink-cli issue +create -t "Bug: 登录失败" -b "复现步骤..."
# 查看 Issue
gitlink-cli issue +view -n 123
gitlink-cli issue +view -i 123
# 添加评论
gitlink-cli issue +comment -i 123 -b "已修复"
# 关闭 Issue
gitlink-cli issue +close -n 123
gitlink-cli issue +close -i 123
# 预览批量关闭 Issue
gitlink-cli issue +batch-close --numbers 123,124 --dry-run
@ -200,17 +220,6 @@ gitlink-cli org +info -i Gitlink
详见: [gitlink-search/SKILL.md](gitlink-search/SKILL.md)
### 场景 5生成维护者驾驶舱
```bash
# Agent 会先只读采集证据,再输出治理计划
gitlink-cli repo +info --owner Gitlink --repo gitlink-cli --format json
gitlink-cli issue +list --owner Gitlink --repo gitlink-cli --state open --format json
gitlink-cli pr +list --owner Gitlink --repo gitlink-cli --state open --format json
```
详见: [gitlink-maintainer-copilot/SKILL.md](gitlink-maintainer-copilot/SKILL.md)
---
## 📚 文档导航
@ -227,23 +236,22 @@ gitlink-cli pr +list --owner Gitlink --repo gitlink-cli --state open --format js
**仓库操作**:
- [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 仓库命令
- [gitlink-branch/SKILL.md](gitlink-branch/SKILL.md) - 分支命令
- [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 完整工作流
**Issue 和 PR**:
- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 命令
- [gitlink-pr/SKILL.md](gitlink-pr/SKILL.md) - PR 命令
- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 工作流
**发布和搜索**:
- [gitlink-release/SKILL.md](gitlink-release/SKILL.md) - Release 命令
- [gitlink-pipeline/SKILL.md](gitlink-pipeline/SKILL.md) - Pipeline 命令
- [gitlink-search/SKILL.md](gitlink-search/SKILL.md) - 搜索命令
**组织和用户**:
- [gitlink-org/SKILL.md](gitlink-org/SKILL.md) - 组织命令
- [gitlink-user/SKILL.md](gitlink-user/SKILL.md) - 用户命令
**维护者驾驶舱**:
- [gitlink-maintainer-copilot/SKILL.md](gitlink-maintainer-copilot/SKILL.md) - 维护者诊断
- [gitlink-maintainer-copilot/examples/maintainer-copilot-workflow.md](gitlink-maintainer-copilot/examples/maintainer-copilot-workflow.md) - 演示流程
---
## ❓ 常见问题
@ -304,7 +312,6 @@ AI 代理可以:
- ✅ 自动分类 Issue
- ✅ 自动生成 Release Notes
- ✅ 自动执行代码审查
- ✅ 自动生成维护者驾驶舱和治理 Issue 草稿
---
@ -316,7 +323,7 @@ AI 代理可以:
- 所有边界情况处理正确
- 完整的文档和示例
可通过 `cd npm && npm test` 验证 Skill 结构和安装脚本。
详见: [../doc/design.md](../doc/design.md)
---
@ -324,6 +331,7 @@ AI 代理可以:
- [主项目 README](../README.md) - gitlink-cli 项目说明
- [设计文档](../doc/design.md) - 架构设计和开发计划
- [API 参考文档](../doc/gitlink_api_reference.md) - GitLink API 参考文档
- [gitlink-bisync](https://www.gitlink.org.cn/wbtiger/gitlink-bisync) - 代码双向同步系统
---

View File

@ -23,7 +23,6 @@ metadata:
| `repo +list` | 仓库列表 | 否(公开项目) |
| `repo +info` | 仓库详情 | 否(公开项目) |
| `repo +readme` | README 内容 | 否(公开项目) |
| `repo +file` | 任意仓库文件内容 | 否(公开项目) |
| `repo +tree` | 仓库文件树 | 否(公开项目) |
| `repo +languages` | 仓库语言统计 | 否(公开项目) |
| `repo +contributors` | 仓库贡献者列表 | 否(公开项目) |
@ -37,10 +36,10 @@ metadata:
| `repo +unlike` | 取消点赞仓库 | 是 |
| `repo +create` | 创建仓库 | 是 |
| `repo +fork` | Fork 仓库 | 是 |
| `repo +delete` | 删除仓库 | 是 |
| `repo +transfer-orgs` | 列出可接收仓库转移的组织 | 是 |
| `repo +transfer` | 发起仓库转移 | 是 |
| `repo +transfer-orgs` | 查看可接收仓库转移的组织 | 是 |
| `repo +transfer` | 发起仓库转移请求 | 是 |
| `repo +transfer-cancel` | 取消待处理的仓库转移 | 是 |
| `repo +delete` | 删除仓库 | 是 |
## 使用示例
@ -58,8 +57,6 @@ 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 +file --owner Gitlink --repo forgeplus --path go.mod --ref master
gitlink-cli repo +file --owner Gitlink --repo forgeplus --path .gitignore --content-only
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
@ -95,8 +92,6 @@ gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-or
# 取消待处理的仓库转移
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --dry-run
# 确认后取消待处理的仓库转移
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --yes
# 删除仓库(⚠️ 危险操作)
@ -125,11 +120,8 @@ gitlink-cli api GET /:owner/:repo/commits --query 'page=1&limit=20'
# 获取标签列表
gitlink-cli api GET /:owner/:repo/tags
# 获取文件内容Shortcut 优先)
gitlink-cli repo +file --owner Gitlink --repo forgeplus --path README.md --ref master
# Raw API 仍可用于未封装场景
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=README.md&ref=master'
# 获取文件内容
gitlink-cli api GET /:owner/:repo/raw/main/README.md
```
## 注意事项
@ -138,4 +130,3 @@ gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=README.md&ref=ma
- `repo +transfer` 会改变仓库所有者,只有显式传入 `--yes` 才会真正发起;执行前先使用 `repo +transfer-orgs` 确认可转移目标,并用 `--dry-run` 预览请求
- `repo +transfer-cancel` 只用于取消已发起且未处理的转移申请,只有显式传入 `--yes` 才会真正执行,建议先用 `--dry-run` 预览
- 创建仓库默认为公开,使用 `--private true` 创建私有仓库
- `repo +file` 只接受文件路径;如果目标是目录,请改用 `repo +tree`