feat(repo): add repo +rename to rename the current repository
Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
This commit is contained in:
parent
9749a4c832
commit
41d2f5bbb0
|
|
@ -0,0 +1,66 @@
|
|||
# repo +rename 仓库重命名命令
|
||||
|
||||
## 背景
|
||||
|
||||
`gitlink-cli repo` 已提供创建、Fork、删除等仓库管理能力,但缺少对标 `gh repo rename` 的重命名命令。用户或 AI Agent 想改名当前仓库时,过去只能手动调用 PATCH 更新项目接口。
|
||||
|
||||
本次变更把重命名封装为 `repo +rename`,与 GitHub CLI 的 `gh repo rename` 语义对齐。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 新增 `gitlink-cli repo +rename` Shortcut。
|
||||
- 调用 `PATCH /{owner}/{repo}` 更新项目,请求体同时设置 `name`(项目名称)与 `identifier`(项目标识)。
|
||||
- 支持 `--name, -n` 指定新名称,必填;空值或纯空白会在调用接口前被拒绝。
|
||||
- 复用现有仓库上下文解析、API 调用和统一输出格式。
|
||||
- 补充中英文 i18n 文案,避免命令帮助信息硬编码。
|
||||
|
||||
## 命令示例
|
||||
|
||||
```bash
|
||||
# 重命名当前仓库(owner/repo 从 git remote 自动解析)
|
||||
gitlink-cli repo +rename --name new-name
|
||||
|
||||
# 显式指定仓库
|
||||
gitlink-cli repo +rename --owner Gitlink --repo forgeplus --name new-name
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--name, -n` | 是 | 新的仓库名称与标识 |
|
||||
| `--owner` | 否 | 全局参数,仓库所有者,可从 git remote 自动解析 |
|
||||
| `--repo` | 否 | 全局参数,仓库名称,可从 git remote 自动解析 |
|
||||
| `--format` | 否 | 全局参数,输出格式:`json`、`table` 或 `yaml` |
|
||||
|
||||
## API 映射
|
||||
|
||||
| Shortcut | Method | API path | 请求体字段 |
|
||||
|----------|--------|----------|-----------|
|
||||
| `repo +rename` | PATCH | `/api/{owner}/{repo}.json` | `name`、`identifier` |
|
||||
|
||||
参照 API 文档「PATCH 更新项目」与「PATCH 更新项目(完整)」,`name` 与 `identifier` 均为必填。`name` 是展示用项目名称,`identifier` 是 URL 标识(等价于 `gh repo rename` 改动的仓库 URL 标识),两者都从 `--name` 取值。
|
||||
|
||||
## 兼容性提示
|
||||
|
||||
重命名会同时修改 `identifier`,也就是仓库的 URL 标识,因此**克隆地址会随之变化**(与 `gh repo rename` 行为一致)。执行后本地已有的 remote 需要相应更新。
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
- 表驱动单元测试断言请求方法为 `PATCH`、路径为 `/owner/repo.json`,且请求体 `name` 与 `identifier` 均为新名称。
|
||||
- 覆盖前后空白被裁剪的场景。
|
||||
- 缺失或纯空白的 `--name` 在调用接口前即返回校验错误,不触发任何 HTTP 请求。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./shortcuts/repo/
|
||||
```
|
||||
|
||||
## 交付要求核对
|
||||
|
||||
- 功能代码:`shortcuts/repo/repo.go`
|
||||
- 单元测试:`shortcuts/repo/repo_test.go`
|
||||
- i18n 文案:`internal/i18n/locales/en-US.json`、`internal/i18n/locales/zh-CN.json`
|
||||
- 变更说明文档:`doc/changes/repo-rename.md`
|
||||
|
|
@ -86,6 +86,7 @@
|
|||
"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.rename.short": "Rename the current repository",
|
||||
"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.",
|
||||
|
|
@ -212,6 +213,7 @@
|
|||
"flag.repo.description": "Repository description",
|
||||
"flag.repo.name": "Repository name",
|
||||
"flag.repo.private": "Make repository private (true/false)",
|
||||
"flag.repo.rename.name": "New repository name and identifier (changes the remote URL)",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@
|
|||
"cmd.repo.fork.short": "Fork 仓库",
|
||||
"cmd.repo.info.short": "显示仓库详情",
|
||||
"cmd.repo.list.short": "列出用户或组织的仓库",
|
||||
"cmd.repo.rename.short": "重命名当前仓库",
|
||||
"cmd.repo.short": "仓库操作",
|
||||
"cmd.repo.tree.short": "列出仓库文件和目录",
|
||||
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
|
||||
|
|
@ -212,6 +213,7 @@
|
|||
"flag.repo.description": "仓库描述",
|
||||
"flag.repo.name": "仓库名称",
|
||||
"flag.repo.private": "设为私有仓库(true/false)",
|
||||
"flag.repo.rename.name": "新的仓库名称与标识(会改变远程 URL)",
|
||||
"flag.repo.tree.path": "要列出的目录路径(默认:仓库根目录)",
|
||||
"flag.repo.tree.ref": "分支、标签或提交引用",
|
||||
"flag.search.keyword": "搜索关键词",
|
||||
|
|
|
|||
|
|
@ -225,6 +225,14 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "rename",
|
||||
Description: tr.T("cmd.repo.rename.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.repo.rename.name"), Required: true},
|
||||
},
|
||||
Run: runRename,
|
||||
},
|
||||
{
|
||||
Name: "fork",
|
||||
Description: tr.T("cmd.repo.fork.short"),
|
||||
|
|
@ -263,6 +271,31 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
|||
return i18n.Default()
|
||||
}
|
||||
|
||||
func runRename(ctx *common.RuntimeContext) error {
|
||||
if _, err := ctx.RequireArg("name"); err != nil {
|
||||
return err
|
||||
}
|
||||
name := strings.TrimSpace(ctx.Arg("name"))
|
||||
if name == "" {
|
||||
return fmt.Errorf("invalid --name: repository name must not be empty")
|
||||
}
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
// The update endpoint requires both the display name and the identifier; the
|
||||
// identifier is the URL slug, so renaming it also changes the repository's
|
||||
// remote clone URL, mirroring `gh repo rename`.
|
||||
body := map[string]interface{}{
|
||||
"name": name,
|
||||
"identifier": name,
|
||||
}
|
||||
env, err := ctx.CallAPI("PATCH", ctx.RepoPath(), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runLanguages(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -488,6 +488,58 @@ func TestRepoCreateWithOptions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- rename ---
|
||||
|
||||
func TestRepoRename(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
arg string
|
||||
wantName string
|
||||
}{
|
||||
{name: "simple name", arg: "new-repo", wantName: "new-repo"},
|
||||
{name: "trims surrounding whitespace", arg: " renamed ", wantName: "renamed"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var body map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "PATCH", "/owner/repo.json")
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode request body: %v", err)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": float64(21),
|
||||
"name": tc.wantName,
|
||||
"identifier": tc.wantName,
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "rename", map[string]string{"name": tc.arg}); err != nil {
|
||||
t.Fatalf("rename failed: %v", err)
|
||||
}
|
||||
// Renaming updates both the display name and the URL identifier, so the
|
||||
// PATCH payload must carry the new name in each field.
|
||||
assertEqual(t, body["name"], tc.wantName)
|
||||
assertEqual(t, body["identifier"], tc.wantName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoRenameRejectsBlankName(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("blank name should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
for _, args := range []map[string]string{nil, {"name": " "}} {
|
||||
if err := runShortcut(t, server, "rename", args); err == nil {
|
||||
t.Fatal("expected validation error for missing or blank name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- validation/error paths ---
|
||||
|
||||
func TestRepoInsightValidation(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue