feat(repo): 增强仓库转移快捷命令安全保护
This commit is contained in:
parent
e9936734ee
commit
9bf0258b43
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 管理
|
||||
|
|
|
|||
|
|
@ -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 已同步补充示例。
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 已保存(用户信息不可用)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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` 创建私有仓库
|
||||
|
|
|
|||
Loading…
Reference in New Issue