From 9bf0258b43f09e0543c71a241d157a1a8f8b74f0 Mon Sep 17 00:00:00 2001 From: Mengz <2567587994@qq.com> Date: Fri, 12 Jun 2026 11:07:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(repo):=20=E5=A2=9E=E5=BC=BA=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E8=BD=AC=E7=A7=BB=E5=BF=AB=E6=8D=B7=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++++- README.zh-CN.md | 8 ++++++- doc/changes/repo-transfer-shortcuts.md | 4 ++-- internal/i18n/locales/en-US.json | 15 +++++++------ internal/i18n/locales/zh-CN.json | 15 +++++++------ shortcuts/repo/repo.go | 15 +++++++++++++ shortcuts/repo/repo_test.go | 31 ++++++++++++++++++++++++-- skills/gitlink-repo/SKILL.md | 9 +++++--- 8 files changed, 82 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index be70a2f..5a2cb50 100644 --- a/README.md +++ b/README.md @@ -262,8 +262,14 @@ 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 -# Cancel a pending repository transfer +# 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 diff --git a/README.zh-CN.md b/README.zh-CN.md index 614e719..0cd4010 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -273,8 +273,14 @@ 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 管理 diff --git a/doc/changes/repo-transfer-shortcuts.md b/doc/changes/repo-transfer-shortcuts.md index 2dcad19..392d1aa 100644 --- a/doc/changes/repo-transfer-shortcuts.md +++ b/doc/changes/repo-transfer-shortcuts.md @@ -8,6 +8,6 @@ 这组命令覆盖仓库所有者侧的转移流程,和用户待办审批侧的接受/拒绝转移申请互补。维护者在迁移项目归属、把个人仓库移交给组织、或由 Agent 编排仓库治理流程时,不再需要手写 Raw API。 -`repo +transfer` 和 `repo +transfer-cancel` 都支持 `--dry-run`,会输出将要请求的 method、path 和 payload,不修改线上数据,便于在执行高风险操作前确认目标仓库和目标所有者。 +`repo +transfer` 和 `repo +transfer-cancel` 都支持 `--dry-run`,会输出将要请求的 method、path 和 payload,不修改线上数据,便于在执行高风险操作前确认目标仓库和目标所有者。真正执行时还要求显式传入 `--yes`,避免在脚本或误操作场景下直接改动仓库归属。 -单元测试覆盖了 API method/path、转移 payload、缺少目标所有者校验,以及 dry-run 不触发远端请求;README、中文 README 和 `gitlink-repo` Skill 已同步补充示例。 +单元测试覆盖了 API method/path、转移 payload、缺少目标所有者校验、缺少 `--yes` 时拒绝执行,以及 dry-run 不触发远端请求;README、中文 README 和 `gitlink-repo` Skill 已同步补充示例。 diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 9f03695..354d779 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -87,6 +87,9 @@ "cmd.repo.info.short": "Show repository details", "cmd.repo.list.short": "List repositories for a user or organization", "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.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.", "cmd.root.short": "GitLink CLI - command-line tool for GitLink", @@ -212,6 +215,10 @@ "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.search.keyword": "Search keyword", @@ -267,11 +274,5 @@ "success.config.set": "✓ {key} = {value}", "warning.auth.not_logged_in": "✗ Not logged in", "warning.auth.token_unverified": "✓ Token stored (but cannot verify: {message})", - "warning.auth.user_unavailable": "✓ Token stored (user info unavailable)", - "cmd.repo.transfer_orgs.short": "List organizations that can receive this repository", - "cmd.repo.transfer.short": "Transfer a repository to another owner", - "cmd.repo.transfer_cancel.short": "Cancel a pending repository transfer", - "flag.repo.target_owner": "Target user or organization login", - "flag.repo.transfer_dry_run": "Preview the transfer request without changing repository ownership", - "flag.repo.transfer_cancel_dry_run": "Preview the cancel request without changing repository transfer state" + "warning.auth.user_unavailable": "✓ Token stored (user info unavailable)" } diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2101c09..9f6b78f 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -87,6 +87,9 @@ "cmd.repo.info.short": "显示仓库详情", "cmd.repo.list.short": "列出用户或组织的仓库", "cmd.repo.short": "仓库操作", + "cmd.repo.transfer.short": "将仓库转移给其他所有者", + "cmd.repo.transfer_cancel.short": "取消待处理的仓库转移", + "cmd.repo.transfer_orgs.short": "列出可接收该仓库的组织", "cmd.repo.tree.short": "列出仓库文件和目录", "cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。", "cmd.root.short": "GitLink CLI - GitLink 命令行工具", @@ -212,6 +215,10 @@ "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.search.keyword": "搜索关键词", @@ -267,11 +274,5 @@ "success.config.set": "✓ 已设置 {key} = {value}", "warning.auth.not_logged_in": "✗ 未登录", "warning.auth.token_unverified": "✓ Token 已保存(但无法验证:{message})", - "warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)", - "cmd.repo.transfer_orgs.short": "列出可接收该仓库的组织", - "cmd.repo.transfer.short": "将仓库转移给其他所有者", - "cmd.repo.transfer_cancel.short": "取消待处理的仓库转移", - "flag.repo.target_owner": "目标用户或组织登录名", - "flag.repo.transfer_dry_run": "预览转移请求,不修改仓库所有者", - "flag.repo.transfer_cancel_dry_run": "预览取消请求,不修改仓库转移状态" + "warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)" } diff --git a/shortcuts/repo/repo.go b/shortcuts/repo/repo.go index 9c3b354..7d3db77 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -260,6 +260,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { 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 { @@ -283,6 +284,9 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { "payload": payload, }) } + if err := requireRepoTransferConfirmation(ctx, "transfer"); err != nil { + return err + } env, err := ctx.CallAPI("POST", path, payload) if err != nil { return err @@ -295,6 +299,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { 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 { @@ -308,6 +313,9 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { "path": path, }) } + if err := requireRepoTransferConfirmation(ctx, "transfer-cancel"); err != nil { + return err + } env, err := ctx.CallAPI("POST", path, nil) if err != nil { return err @@ -340,6 +348,13 @@ func repoTransferPath(ctx *common.RuntimeContext, action string) string { 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] diff --git a/shortcuts/repo/repo_test.go b/shortcuts/repo/repo_test.go index 8a8cb0c..80da629 100644 --- a/shortcuts/repo/repo_test.go +++ b/shortcuts/repo/repo_test.go @@ -456,7 +456,10 @@ func TestRepoTransfer(t *testing.T) { })) defer server.Close() - err := runShortcut(t, server, "transfer", map[string]string{"target-owner": " target-org "}) + err := runShortcut(t, server, "transfer", map[string]string{ + "target-owner": " target-org ", + "yes": "true", + }) if err != nil { t.Fatalf("transfer failed: %v", err) } @@ -478,6 +481,18 @@ func TestRepoTransferDryRunDoesNotCallAPI(t *testing.T) { } } +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") @@ -509,7 +524,7 @@ func TestRepoTransferCancel(t *testing.T) { })) defer server.Close() - if err := runShortcut(t, server, "transfer-cancel", nil); err != nil { + if err := runShortcut(t, server, "transfer-cancel", map[string]string{"yes": "true"}); err != nil { t.Fatalf("transfer-cancel failed: %v", err) } } @@ -526,6 +541,18 @@ func TestRepoTransferCancelDryRunDoesNotCallAPI(t *testing.T) { } } +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 { diff --git a/skills/gitlink-repo/SKILL.md b/skills/gitlink-repo/SKILL.md index d12723f..007e5cb 100644 --- a/skills/gitlink-repo/SKILL.md +++ b/skills/gitlink-repo/SKILL.md @@ -88,11 +88,14 @@ 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 +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 + # 删除仓库(⚠️ 危险操作) gitlink-cli repo +delete --owner myuser --repo old-project ``` @@ -115,6 +118,6 @@ gitlink-cli api GET /:owner/:repo/raw/main/README.md ## 注意事项 - `repo +delete` 是不可逆操作,执行前必须确认用户意图 -- `repo +transfer` 会改变仓库所有者,执行前先使用 `repo +transfer-orgs` 确认可转移目标,并用 `--dry-run` 预览请求 -- `repo +transfer-cancel` 只用于取消已发起且未处理的转移申请,建议先用 `--dry-run` 预览 +- `repo +transfer` 会改变仓库所有者,只有显式传入 `--yes` 才会真正发起;执行前先使用 `repo +transfer-orgs` 确认可转移目标,并用 `--dry-run` 预览请求 +- `repo +transfer-cancel` 只用于取消已发起且未处理的转移申请,只有显式传入 `--yes` 才会真正执行,建议先用 `--dry-run` 预览 - 创建仓库默认为公开,使用 `--private true` 创建私有仓库