diff --git a/doc/changes/notification-shortcut.md b/doc/changes/notification-shortcut.md new file mode 100644 index 0000000..7850c6b --- /dev/null +++ b/doc/changes/notification-shortcut.md @@ -0,0 +1,12 @@ +# notification shortcut + +新增 `notification` 命令组,支持用户通知管理: + +| 命令 | 功能 | +|------|------| +| `notification +list` | 列出通知列表 | +| `notification +view --id ` | 查看通知详情 | +| `notification +read --id ` | 标记通知已读 | +| `notification +delete --id ` | 删除通知 | + +修复:`issue +create` 命令的 `--label` 参数现在会正确传入请求 body。 diff --git a/shortcuts/issue/issue.go b/shortcuts/issue/issue.go index 7c0bc68..48a70e4 100644 --- a/shortcuts/issue/issue.go +++ b/shortcuts/issue/issue.go @@ -82,6 +82,9 @@ func Shortcuts() []*common.Shortcut { if m := ctx.Arg("milestone"); m != "" { body["fixed_version_id"] = m } + if l := ctx.Arg("label"); l != "" { + body["label_id"] = l + } env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body) if err != nil { return err diff --git a/shortcuts/notification/notification.go b/shortcuts/notification/notification.go new file mode 100644 index 0000000..eb9a14d --- /dev/null +++ b/shortcuts/notification/notification.go @@ -0,0 +1,110 @@ +package notification + +import ( + "fmt" + "net/url" + + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +// Shortcuts returns notification management shortcuts. +func Shortcuts() []*common.Shortcut { + return []*common.Shortcut{ + { + Name: "list", + Description: "List notifications", + Flags: []common.Flag{ + {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 { + login, err := resolveLogin(ctx) + 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("/users/%s/messages", login), q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "view", + Description: "View notification details", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := resolveLogin(ctx) + if err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/messages/%s", login, id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "read", + Description: "Mark a notification as read", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := resolveLogin(ctx) + if err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("PATCH", fmt.Sprintf("/users/%s/messages/%s", login, id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "delete", + Description: "Delete a notification", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := resolveLogin(ctx) + if err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/users/%s/messages/%s", login, id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + } +} + +// resolveLogin returns the user login from the runtime context. +func resolveLogin(ctx *common.RuntimeContext) (string, error) { + if ctx.Owner == "" { + return "", fmt.Errorf("provide --owner (your login) or set it via config") + } + return ctx.Owner, nil +} diff --git a/shortcuts/notification/notification_test.go b/shortcuts/notification/notification_test.go new file mode 100644 index 0000000..4466c62 --- /dev/null +++ b/shortcuts/notification/notification_test.go @@ -0,0 +1,110 @@ +package notification + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func TestNotificationList(t *testing.T) { + server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/users/testuser/messages.json") + writeJSON(t, w, map[string]interface{}{"total_count": 1, "messages": []interface{}{}}) + }) + defer server.Close() + + if err := runNotificationShortcut(t, server, "list", nil); err != nil { + t.Fatalf("list shortcut failed: %v", err) + } +} + +func TestNotificationView(t *testing.T) { + server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/users/testuser/messages/42.json") + writeJSON(t, w, map[string]interface{}{"id": 42, "subject": "test notification"}) + }) + defer server.Close() + + if err := runNotificationShortcut(t, server, "view", map[string]string{"id": "42"}); err != nil { + t.Fatalf("view shortcut failed: %v", err) + } +} + +func TestNotificationRead(t *testing.T) { + server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "PATCH", "/users/testuser/messages/42.json") + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + if err := runNotificationShortcut(t, server, "read", map[string]string{"id": "42"}); err != nil { + t.Fatalf("read shortcut failed: %v", err) + } +} + +func TestNotificationDelete(t *testing.T) { + server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "DELETE", "/users/testuser/messages/42.json") + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + if err := runNotificationShortcut(t, server, "delete", map[string]string{"id": "42"}); err != nil { + t.Fatalf("delete shortcut failed: %v", err) + } +} + +// --- test helpers --- + +func runNotificationShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + shortcut := findNotificationShortcut(t, name) + ctx := &common.RuntimeContext{ + Client: &client.Client{ + HTTP: server.Client(), + BaseURL: server.URL, + }, + Owner: "testuser", + Format: "json", + Args: args, + } + if ctx.Args == nil { + ctx.Args = map[string]string{} + } + return shortcut.Run(ctx) +} + +func findNotificationShortcut(t *testing.T, name string) *common.Shortcut { + t.Helper() + for _, shortcut := range Shortcuts() { + if shortcut.Name == name { + return shortcut + } + } + t.Fatalf("shortcut %q not found", name) + return nil +} + +func newNotificationTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { + t.Helper() + return httptest.NewServer(handler) +} + +func assertRequest(t *testing.T, r *http.Request, method, path string) { + t.Helper() + if r.Method != method || r.URL.Path != path { + t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path) + } +} + +func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(payload); err != nil { + t.Fatalf("failed to write response: %v", err) + } +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 3b3d8be..bc42996 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -10,6 +10,7 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/compare" "github.com/gitlink-org/gitlink-cli/shortcuts/issue" "github.com/gitlink-org/gitlink-cli/shortcuts/member" + "github.com/gitlink-org/gitlink-cli/shortcuts/notification" "github.com/gitlink-org/gitlink-cli/shortcuts/milestone" "github.com/gitlink-org/gitlink-cli/shortcuts/org" "github.com/gitlink-org/gitlink-cli/shortcuts/pr" @@ -41,6 +42,7 @@ func RegisterAll(root *cobra.Command) { "workflow": workflow.Shortcuts(), "wiki": wiki.Shortcuts(), "label": label.Shortcuts(), + "notification": notification.Shortcuts(), } descriptions := map[string]string{ @@ -60,6 +62,7 @@ func RegisterAll(root *cobra.Command) { "workflow": "AI agent workflow analysis", "wiki": "Wiki page operations", "label": "Label operations", + "notification": "Notification operations", } for name, shortcuts := range groups {