Merge pull request #412: feat(issue): 新增 issue +delete 单议题删除
# Conflicts: # shortcuts/issue/issue_test.go
This commit is contained in:
commit
4e6cc86343
|
|
@ -350,6 +350,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
|
||||
|
||||
|
|
|
|||
|
|
@ -361,6 +361,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
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,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.reopen.short": "Reopen a closed issue",
|
||||
"cmd.issue.short": "Issue operations",
|
||||
|
|
@ -168,6 +169,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",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@
|
|||
"cmd.issue.close.short": "关闭议题",
|
||||
"cmd.issue.comment.short": "给议题添加评论",
|
||||
"cmd.issue.create.short": "创建新议题",
|
||||
"cmd.issue.delete.short": "删除议题",
|
||||
"cmd.issue.list.short": "列出议题",
|
||||
"cmd.issue.reopen.short": "重新打开已关闭的议题",
|
||||
"cmd.issue.short": "议题操作",
|
||||
|
|
@ -168,6 +169,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",
|
||||
|
|
|
|||
|
|
@ -195,6 +195,30 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return setIssueStatus(ctx, 1) // 1 = open
|
||||
},
|
||||
},
|
||||
{
|
||||
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"),
|
||||
|
|
|
|||
|
|
@ -417,114 +417,35 @@ func TestIssueCloseFetchFails(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- reopen ---
|
||||
// --- delete ---
|
||||
|
||||
func TestIssueReopen(t *testing.T) {
|
||||
var patchPayload map[string]interface{}
|
||||
func TestIssueDelete(t *testing.T) {
|
||||
var deletedPath string
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": float64(42),
|
||||
"subject": "Existing title",
|
||||
"description": "Existing description",
|
||||
"status": map[string]interface{}{"id": 5},
|
||||
})
|
||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
patchPayload = decodeJSON(t, r)
|
||||
writeJSON(t, w, patchPayload)
|
||||
default:
|
||||
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, "reopen", map[string]string{"number": "42"})
|
||||
err := runShortcut(t, server, "delete", map[string]string{"number": "42", "yes": "true"})
|
||||
if err != nil {
|
||||
t.Fatalf("reopen failed: %v", err)
|
||||
t.Fatalf("delete failed: %v", err)
|
||||
}
|
||||
assertEqual(t, patchPayload["subject"], "Existing title")
|
||||
assertEqual(t, patchPayload["description"], "Existing description")
|
||||
assertEqual(t, patchPayload["status_id"], float64(1))
|
||||
assertEqual(t, deletedPath, "/v1/owner/repo/issues/42.json")
|
||||
}
|
||||
|
||||
func TestIssueReopenPreservesCurrentMetadata(t *testing.T) {
|
||||
var patchPayload map[string]interface{}
|
||||
func TestIssueDeleteRequiresConfirmation(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"subject": "Existing title",
|
||||
"status": map[string]interface{}{"id": 5},
|
||||
"priority": map[string]interface{}{"id": 3},
|
||||
"tags": []map[string]interface{}{
|
||||
{"id": 4},
|
||||
},
|
||||
"assigners": []map[string]interface{}{
|
||||
{"id": 7},
|
||||
},
|
||||
"branch_name": "feature/x",
|
||||
"start_date": "2026-01-01",
|
||||
"due_date": "2026-02-01",
|
||||
})
|
||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
patchPayload = decodeJSON(t, r)
|
||||
writeJSON(t, w, patchPayload)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
t.Fatalf("unexpected request without --yes: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "reopen", map[string]string{"number": "42"})
|
||||
if err != nil {
|
||||
t.Fatalf("reopen shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, patchPayload["subject"], "Existing title")
|
||||
assertEqual(t, patchPayload["status_id"], float64(1))
|
||||
assertEqual(t, patchPayload["priority_id"], float64(3))
|
||||
assertNumberSlice(t, patchPayload["issue_tag_ids"], []float64{4})
|
||||
assertNumberSlice(t, patchPayload["assigner_ids"], []float64{7})
|
||||
assertEqual(t, patchPayload["branch_name"], "feature/x")
|
||||
assertEqual(t, patchPayload["start_date"], "2026-01-01")
|
||||
assertEqual(t, patchPayload["due_date"], "2026-02-01")
|
||||
}
|
||||
|
||||
func TestIssueReopenAcceptsIDAlias(t *testing.T) {
|
||||
var patchPayload map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"subject": "Existing title",
|
||||
"description": "Existing description",
|
||||
})
|
||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
patchPayload = decodeJSON(t, r)
|
||||
writeJSON(t, w, patchPayload)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "reopen", map[string]string{"id": "42"})
|
||||
if err != nil {
|
||||
t.Fatalf("reopen shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, patchPayload["status_id"], float64(1))
|
||||
}
|
||||
|
||||
func TestIssueReopenFetchFails(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
writeJSON(t, w, map[string]interface{}{"error": "not found"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "reopen", map[string]string{"number": "999"})
|
||||
err := runShortcut(t, server, "delete", map[string]string{"number": "42"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when issue not found")
|
||||
t.Fatal("expected error without --yes confirmation")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -852,7 +773,6 @@ func TestIssueNumberOrIDIsRequired(t *testing.T) {
|
|||
}{
|
||||
{name: "view", args: map[string]string{}},
|
||||
{name: "close", args: map[string]string{}},
|
||||
{name: "reopen", args: map[string]string{}},
|
||||
{name: "update", args: map[string]string{"title": "New title"}},
|
||||
{name: "comment", args: map[string]string{"body": "Fixed"}},
|
||||
}
|
||||
|
|
@ -871,7 +791,7 @@ func TestIssueNumberOrIDIsRequired(t *testing.T) {
|
|||
|
||||
// --- batch-close ---
|
||||
|
||||
func TestBatchClosePreservesCurrentMetadata(t *testing.T) {
|
||||
func TestBatchClosePreservesCurrentDescription(t *testing.T) {
|
||||
var updatePayload map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
|
|
@ -879,16 +799,6 @@ func TestBatchClosePreservesCurrentMetadata(t *testing.T) {
|
|||
writeJSON(t, w, map[string]interface{}{
|
||||
"subject": "Existing title",
|
||||
"description": "Existing description",
|
||||
"priority": map[string]interface{}{"id": 3},
|
||||
"tags": []map[string]interface{}{
|
||||
{"id": 4},
|
||||
},
|
||||
"assigners": []map[string]interface{}{
|
||||
{"id": 5},
|
||||
},
|
||||
"branch_name": "release/next",
|
||||
"start_date": "2026-05-01",
|
||||
"due_date": "2026-05-31",
|
||||
})
|
||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
updatePayload = decodeJSON(t, r)
|
||||
|
|
@ -909,12 +819,6 @@ func TestBatchClosePreservesCurrentMetadata(t *testing.T) {
|
|||
assertEqual(t, updatePayload["subject"], "Existing title")
|
||||
assertEqual(t, updatePayload["description"], "Existing description")
|
||||
assertEqual(t, updatePayload["status_id"], float64(5))
|
||||
assertEqual(t, updatePayload["priority_id"], float64(3))
|
||||
assertNumberSlice(t, updatePayload["issue_tag_ids"], []float64{4})
|
||||
assertNumberSlice(t, updatePayload["assigner_ids"], []float64{5})
|
||||
assertEqual(t, updatePayload["branch_name"], "release/next")
|
||||
assertEqual(t, updatePayload["start_date"], "2026-05-01")
|
||||
assertEqual(t, updatePayload["due_date"], "2026-05-31")
|
||||
}
|
||||
|
||||
func TestBatchCloseDryRun(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue