feat(issue): add issue +delete with --yes confirmation

Single-issue deletion via DELETE /api/v1/:owner/:repo/issues/:number.
The existing +batch-delete requires repo manager permissions and global
database IDs; +delete works for the issue author using the web issue
number. Destructive, so it refuses to run without --yes.

Includes en/zh i18n keys, README examples, and unit tests.
Production-verified on gitlink.org.cn (create -> delete -> 404 view).
This commit is contained in:
Taoyouce 2026-07-10 11:39:12 +00:00
parent 9749a4c832
commit a36e9c92c3
6 changed files with 66 additions and 0 deletions

View File

@ -339,6 +339,9 @@ gitlink-cli issue +update --owner Gitlink --repo forgeplus --number 123 --priori
# Close an issue
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
# Delete an issue (destructive; requires --yes)
gitlink-cli issue +delete --owner Gitlink --repo forgeplus --number 123 --yes
# Preview batch close without changing data
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run

View File

@ -350,6 +350,9 @@ gitlink-cli issue +update --owner Gitlink --repo forgeplus --number 123 --priori
# 关闭 Issue
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
# 删除 Issue破坏性操作需 --yes 确认)
gitlink-cli issue +delete --owner Gitlink --repo forgeplus --number 123 --yes
# 预览批量关闭,不修改数据
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run

View File

@ -43,6 +43,7 @@
"cmd.issue.close.short": "Close an issue",
"cmd.issue.comment.short": "Add a comment to an issue",
"cmd.issue.create.short": "Create a new issue",
"cmd.issue.delete.short": "Delete an issue",
"cmd.issue.list.short": "List issues",
"cmd.issue.short": "Issue operations",
"cmd.issue.update.short": "Update an issue",
@ -158,6 +159,7 @@
"flag.issue.batch_list.limit": "Maximum issues to return, capped at 100",
"flag.issue.batch_process.limit": "Maximum issues to process, capped at 100",
"flag.issue.body": "Issue description",
"flag.issue.delete.yes": "Confirm issue deletion",
"flag.issue.label": "Label ID",
"flag.issue.label_filter": "Filter by existing label",
"flag.issue.milestone": "Milestone ID",

View File

@ -43,6 +43,7 @@
"cmd.issue.close.short": "关闭议题",
"cmd.issue.comment.short": "给议题添加评论",
"cmd.issue.create.short": "创建新议题",
"cmd.issue.delete.short": "删除议题",
"cmd.issue.list.short": "列出议题",
"cmd.issue.short": "议题操作",
"cmd.issue.update.short": "更新议题",
@ -158,6 +159,7 @@
"flag.issue.batch_list.limit": "最多返回的议题数,上限 100",
"flag.issue.batch_process.limit": "最多处理的议题数,上限 100",
"flag.issue.body": "议题描述",
"flag.issue.delete.yes": "确认删除议题",
"flag.issue.label": "标签 ID",
"flag.issue.label_filter": "按已有标签筛选",
"flag.issue.milestone": "里程碑 ID",

View File

@ -207,6 +207,30 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "delete",
Description: tr.T("cmd.issue.delete.short"),
Flags: appendIssueNumberFlags(
common.Flag{Name: "yes", Usage: tr.T("flag.issue.delete.yes"), Bool: true, Default: "false"},
),
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
number, err := issueNumberArg(ctx)
if err != nil {
return err
}
if !parseBool(ctx.Arg("yes")) {
return fmt.Errorf("delete is destructive; pass --yes to confirm deleting issue #%s", number)
}
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "update",
Description: tr.T("cmd.issue.update.short"),

View File

@ -417,6 +417,38 @@ func TestIssueCloseFetchFails(t *testing.T) {
}
}
// --- delete ---
func TestIssueDelete(t *testing.T) {
var deletedPath string
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
deletedPath = r.URL.Path
writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"})
})
defer server.Close()
err := runShortcut(t, server, "delete", map[string]string{"number": "42", "yes": "true"})
if err != nil {
t.Fatalf("delete failed: %v", err)
}
assertEqual(t, deletedPath, "/v1/owner/repo/issues/42.json")
}
func TestIssueDeleteRequiresConfirmation(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("unexpected request without --yes: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runShortcut(t, server, "delete", map[string]string{"number": "42"})
if err == nil {
t.Fatal("expected error without --yes confirmation")
}
}
// --- update ---
func TestIssueUpdateTitle(t *testing.T) {