forked from Gitlink/gitlink-cli
feat(issue): add comments CRUD, delete, batch-update, batch-destroy shortcuts
This commit is contained in:
parent
94661010ac
commit
a94088e4ee
|
|
@ -223,6 +223,199 @@ func Shortcuts() []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "comments",
|
||||
Description: "List comments and activity on an issue",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update-comment",
|
||||
Description: "Update a comment on an issue",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
{Name: "id", Usage: "Comment journal ID", Required: true},
|
||||
{Name: "body", Short: "b", Usage: "New comment body", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := ctx.RequireArg("body")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"notes": body,
|
||||
}
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s/journals/%s", v1RepoPath(ctx), number, id), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete-comment",
|
||||
Description: "Delete a comment on an issue",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
{Name: "id", Usage: "Comment journal ID", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s/journals/%s", v1RepoPath(ctx), number, id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "reply-comment",
|
||||
Description: "List child replies of a comment",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
{Name: "id", Usage: "Parent comment journal ID", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s/journals/%s/children_journals", v1RepoPath(ctx), number, id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: "Delete an issue",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "batch-update",
|
||||
Description: "Batch update multiple issues",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ids", Usage: "Comma-separated issue IDs", Required: true},
|
||||
{Name: "state", Short: "s", Usage: "New state: open, closed"},
|
||||
{Name: "assignee", Short: "a", Usage: "Assignee login"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
idsStr, err := ctx.RequireArg("ids")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"ids": parseIDList(idsStr),
|
||||
}
|
||||
if s := ctx.Arg("state"); s != "" {
|
||||
statusID, err := normalizeIssueStatus(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body["status_id"] = statusID
|
||||
}
|
||||
if a := ctx.Arg("assignee"); a != "" {
|
||||
body["assigned_to_id"] = a
|
||||
}
|
||||
env, err := ctx.CallAPI("PATCH", v1RepoPath(ctx)+"/issues/batch_update", body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "batch-destroy",
|
||||
Description: "Batch delete multiple issues",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ids", Usage: "Comma-separated issue IDs", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
idsStr, err := ctx.RequireArg("ids")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"ids": parseIDList(idsStr),
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", v1RepoPath(ctx)+"/issues/batch_destroy", body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,3 +482,19 @@ func normalizeIssueStatus(state string) (interface{}, error) {
|
|||
return nil, fmt.Errorf("invalid --state %q: use open, closed, or a numeric status_id", state)
|
||||
}
|
||||
}
|
||||
|
||||
// parseIDList splits a comma-separated string into an int slice.
|
||||
func parseIDList(s string) []int {
|
||||
parts := strings.Split(s, ",")
|
||||
ids := make([]int, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if id, err := strconv.Atoi(p); err == nil {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,3 +186,182 @@ func assertEqual(t *testing.T, got interface{}, want interface{}) {
|
|||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueDelete(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "DELETE" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
writeJSON(t, w, map[string]interface{}{"ok": true})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "delete", map[string]string{"number": "42"})
|
||||
if err != nil {
|
||||
t.Fatalf("delete shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueComments(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/journals.json":
|
||||
if r.URL.Query().Get("page") != "2" || r.URL.Query().Get("limit") != "10" {
|
||||
t.Fatalf("unexpected query params: %v", r.URL.Query())
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"journals": []interface{}{
|
||||
map[string]interface{}{"id": 1, "notes": "first comment"},
|
||||
map[string]interface{}{"id": 2, "notes": "second comment"},
|
||||
},
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "comments", map[string]string{
|
||||
"number": "42",
|
||||
"page": "2",
|
||||
"limit": "10",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("comments shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueUpdateComment(t *testing.T) {
|
||||
var updatePayload map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42/journals/99.json":
|
||||
updatePayload = decodeJSON(t, r)
|
||||
writeJSON(t, w, updatePayload)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "update-comment", map[string]string{
|
||||
"number": "42",
|
||||
"id": "99",
|
||||
"body": "updated comment text",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update-comment shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertEqual(t, updatePayload["notes"], "updated comment text")
|
||||
}
|
||||
|
||||
func TestIssueDeleteComment(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "DELETE" && r.URL.Path == "/v1/owner/repo/issues/42/journals/99.json":
|
||||
writeJSON(t, w, map[string]interface{}{"ok": true})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "delete-comment", map[string]string{
|
||||
"number": "42",
|
||||
"id": "99",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("delete-comment shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueReplyComment(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/journals/99/children_journals.json":
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"journals": []interface{}{
|
||||
map[string]interface{}{"id": 100, "notes": "reply"},
|
||||
},
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "reply-comment", map[string]string{
|
||||
"number": "42",
|
||||
"id": "99",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("reply-comment shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueBatchUpdate(t *testing.T) {
|
||||
var updatePayload map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/batch_update.json":
|
||||
updatePayload = decodeJSON(t, r)
|
||||
writeJSON(t, w, updatePayload)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "batch-update", map[string]string{
|
||||
"ids": "1,2,3",
|
||||
"state": "closed",
|
||||
"assignee": "someone",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-update shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
ids, ok := updatePayload["ids"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected ids to be a slice, got %T", updatePayload["ids"])
|
||||
}
|
||||
assertEqual(t, len(ids), 3)
|
||||
assertEqual(t, ids[0], float64(1))
|
||||
assertEqual(t, ids[1], float64(2))
|
||||
assertEqual(t, ids[2], float64(3))
|
||||
assertEqual(t, updatePayload["status_id"], float64(5))
|
||||
assertEqual(t, updatePayload["assigned_to_id"], "someone")
|
||||
}
|
||||
|
||||
func TestIssueBatchDestroy(t *testing.T) {
|
||||
var updatePayload map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "DELETE" && r.URL.Path == "/v1/owner/repo/issues/batch_destroy.json":
|
||||
updatePayload = decodeJSON(t, r)
|
||||
writeJSON(t, w, updatePayload)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "batch-destroy", map[string]string{
|
||||
"ids": "10,20,30",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-destroy shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
ids, ok := updatePayload["ids"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected ids to be a slice, got %T", updatePayload["ids"])
|
||||
}
|
||||
assertEqual(t, len(ids), 3)
|
||||
assertEqual(t, ids[0], float64(10))
|
||||
assertEqual(t, ids[1], float64(20))
|
||||
assertEqual(t, ids[2], float64(30))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue