diff --git a/README.md b/README.md index cd25dce..646c03c 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans | 🏢 Org | Manage organizations, members, teams | | 🔧 CI | View builds, logs, CI/CD operations | | ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs | +| 🔔 Message Settings | Inspect and update personal message delivery preferences | | 🔔 Webhook | Manage repo webhooks and test deliveries | | 📖 Wiki | List, view, create, update, and delete wiki pages | | 🔍 Search | Search repositories, users | diff --git a/README.zh-CN.md b/README.zh-CN.md index 6a8879d..4de51dc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -113,6 +113,7 @@ | 🏢 组织 | 管理组织、成员、团队 | | 🔧 CI | 查看构建、日志、CI/CD 操作 | | ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 | +| 🔔 消息通知设置 | 查看并更新个人消息通知投递偏好 | | 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 | | 🔍 搜索 | 搜索仓库、用户 | | 📊 数据集 | 按项目查询科研数据集 | @@ -268,6 +269,29 @@ gitlink-cli repo +create -n my-project -d "项目描述" gitlink-cli repo +fork --owner Gitlink --repo forgeplus ``` +### 消息通知设置 + +```bash +# 列出可用的消息通知设置分组和键 +gitlink-cli message-settings +catalog + +# 查看当前用户生效中的消息通知设置 +gitlink-cli message-settings +view + +# 只看另一个用户的仓库管理类消息设置 +gitlink-cli message-settings +view --login Mengz --group ManageProject + +# 预览关闭指定设置键的站内通知,不发送请求 +gitlink-cli message-settings +update \ + --channel notification \ + --state off \ + --keys Normal::Permission,ManageProject::Issue \ + --dry-run + +# 将预设应用到所有已知设置 +gitlink-cli message-settings +preset --name notification-only --all +``` + ### Webhook 管理 ```bash diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..9978bcf 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -47,6 +47,11 @@ "cmd.issue.short": "Issue operations", "cmd.issue.update.short": "Update an issue", "cmd.issue.view.short": "View issue details", + "cmd.message_settings.catalog.short": "List available message setting groups and keys", + "cmd.message_settings.preset.short": "Apply a preset to selected message settings", + "cmd.message_settings.short": "Message settings operations", + "cmd.message_settings.update.short": "Update selected message settings while preserving other values", + "cmd.message_settings.view.short": "Show effective message settings for a user", "cmd.org.create.short": "Create an organization", "cmd.org.info.short": "Show organization details", "cmd.org.list.short": "List organizations", @@ -173,6 +178,13 @@ "flag.issue.title": "Issue title", "flag.lang": "Display language", "flag.limit": "Items per page", + "flag.message_settings.all": "Apply to all known setting keys", + "flag.message_settings.channel": "Channel to change: notification, email, or both", + "flag.message_settings.group": "Filter or select groups by short name, for example: Normal,ManageProject", + "flag.message_settings.keys": "Comma-separated setting keys, for example: Normal::Permission,ManageProject::Issue", + "flag.message_settings.login": "Target user login (defaults to current authenticated user)", + "flag.message_settings.preset_name": "Preset name: all-on, all-off, notification-only, email-only", + "flag.message_settings.state": "Desired state: on/off, true/false, enable/disable", "flag.org.id": "Organization ID", "flag.org.id_or_login": "Organization ID or login", "flag.org.name": "Organization name", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..80b92cc 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -47,6 +47,11 @@ "cmd.issue.short": "议题操作", "cmd.issue.update.short": "更新议题", "cmd.issue.view.short": "查看议题详情", + "cmd.message_settings.catalog.short": "列出可用的消息通知设置分组和键", + "cmd.message_settings.preset.short": "将预设应用到选中的消息通知设置", + "cmd.message_settings.short": "消息通知设置操作", + "cmd.message_settings.update.short": "在保留其他值的前提下更新选中的消息通知设置", + "cmd.message_settings.view.short": "查看用户当前生效的消息通知设置", "cmd.org.create.short": "创建组织", "cmd.org.info.short": "显示组织详情", "cmd.org.list.short": "列出组织", @@ -173,6 +178,13 @@ "flag.issue.title": "议题标题", "flag.lang": "显示语言", "flag.limit": "每页条目数", + "flag.message_settings.all": "应用到所有已知的设置键", + "flag.message_settings.channel": "要修改的通道:notification、email 或 both", + "flag.message_settings.group": "按短分组名筛选或选中分组,例如:Normal,ManageProject", + "flag.message_settings.keys": "逗号分隔的设置键,例如:Normal::Permission,ManageProject::Issue", + "flag.message_settings.login": "目标用户登录名(默认:当前认证用户)", + "flag.message_settings.preset_name": "预设名称:all-on、all-off、notification-only、email-only", + "flag.message_settings.state": "目标状态:on/off、true/false、enable/disable", "flag.org.id": "组织 ID", "flag.org.id_or_login": "组织 ID 或登录名", "flag.org.name": "组织名称", diff --git a/shortcuts/messagesetting/messagesetting.go b/shortcuts/messagesetting/messagesetting.go index b87d021..0552c18 100644 --- a/shortcuts/messagesetting/messagesetting.go +++ b/shortcuts/messagesetting/messagesetting.go @@ -6,6 +6,7 @@ import ( "sort" "strings" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -60,10 +61,10 @@ type settingRegistry struct { } type groupedSettingOutput struct { - Group string `json:"group"` - GroupName string `json:"group_name,omitempty"` - Settings []settingOutput `json:"settings"` - Summary channelSummaryOutput `json:"summary"` + Group string `json:"group"` + GroupName string `json:"group_name,omitempty"` + Settings []settingOutput `json:"settings"` + Summary channelSummaryOutput `json:"summary"` } type settingOutput struct { @@ -107,55 +108,63 @@ var allowedPresetNames = map[string]presetSpec{ } // Shortcuts returns message settings shortcuts. -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "catalog", - Description: "List available message setting groups and keys", + Description: tr.T("cmd.message_settings.catalog.short"), Flags: []common.Flag{ - {Name: "group", Usage: "Filter groups by short name, for example: Normal,ManageProject"}, + {Name: "group", Usage: tr.T("flag.message_settings.group")}, }, Run: runCatalog, }, { Name: "view", - Description: "Show effective message settings for a user", + Description: tr.T("cmd.message_settings.view.short"), Flags: []common.Flag{ - {Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"}, - {Name: "group", Usage: "Filter groups by short name, for example: Normal,ManageProject"}, + {Name: "login", Short: "l", Usage: tr.T("flag.message_settings.login")}, + {Name: "group", Usage: tr.T("flag.message_settings.group")}, }, Run: runView, }, { Name: "update", - Description: "Update message settings for selected keys while preserving other values", + Description: tr.T("cmd.message_settings.update.short"), Flags: []common.Flag{ - {Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"}, - {Name: "channel", Usage: "Channel to change: notification, email, or both", Required: true}, - {Name: "state", Usage: "Desired state: on/off, true/false, enable/disable", Required: true}, - {Name: "keys", Usage: "Comma-separated setting keys, for example: Normal::Permission,ManageProject::Issue"}, - {Name: "group", Usage: "Apply to all keys in one or more groups, for example: Normal,ManageProject"}, - {Name: "all", Usage: "Apply to all known setting keys", Bool: true, Default: "false"}, - {Name: "dry-run", Usage: "Preview the update without sending it", Bool: true, Default: "false"}, + {Name: "login", Short: "l", Usage: tr.T("flag.message_settings.login")}, + {Name: "channel", Usage: tr.T("flag.message_settings.channel"), Required: true}, + {Name: "state", Usage: tr.T("flag.message_settings.state"), Required: true}, + {Name: "keys", Usage: tr.T("flag.message_settings.keys")}, + {Name: "group", Usage: tr.T("flag.message_settings.group")}, + {Name: "all", Usage: tr.T("flag.message_settings.all"), Bool: true, Default: "false"}, + {Name: "dry-run", Usage: tr.T("flag.dry_run"), Bool: true, Default: "false"}, }, Run: runUpdate, }, { Name: "preset", - Description: "Apply a preset to selected message settings", + Description: tr.T("cmd.message_settings.preset.short"), Flags: []common.Flag{ - {Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"}, - {Name: "name", Usage: "Preset name: all-on, all-off, notification-only, email-only", Required: true}, - {Name: "keys", Usage: "Comma-separated setting keys, for example: Normal::Permission,ManageProject::Issue"}, - {Name: "group", Usage: "Apply to all keys in one or more groups, for example: Normal,ManageProject"}, - {Name: "all", Usage: "Apply to all known setting keys", Bool: true, Default: "false"}, - {Name: "dry-run", Usage: "Preview the update without sending it", Bool: true, Default: "false"}, + {Name: "login", Short: "l", Usage: tr.T("flag.message_settings.login")}, + {Name: "name", Usage: tr.T("flag.message_settings.preset_name"), Required: true}, + {Name: "keys", Usage: tr.T("flag.message_settings.keys")}, + {Name: "group", Usage: tr.T("flag.message_settings.group")}, + {Name: "all", Usage: tr.T("flag.message_settings.all"), Bool: true, Default: "false"}, + {Name: "dry-run", Usage: tr.T("flag.dry_run"), Bool: true, Default: "false"}, }, Run: runPreset, }, } } +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} + func runCatalog(ctx *common.RuntimeContext) error { registry, err := loadRegistry(ctx) if err != nil { diff --git a/shortcuts/messagesetting/messagesetting_test.go b/shortcuts/messagesetting/messagesetting_test.go index 147d7ee..9085dea 100644 --- a/shortcuts/messagesetting/messagesetting_test.go +++ b/shortcuts/messagesetting/messagesetting_test.go @@ -18,6 +18,9 @@ func runMessageSettingShortcut(t *testing.T, server *httptest.Server, name strin Format: "json", Args: args, } + if ctx.Args == nil { + ctx.Args = map[string]string{} + } return shortcut.Run(ctx) } @@ -182,9 +185,9 @@ func TestMessageSettingsPresetPostsSelectedKeys(t *testing.T) { defer server.Close() err := runMessageSettingShortcut(t, server, "preset", map[string]string{ - "name": "all-off", - "keys": "Permission,ManageProject::Issue", - "all": "false", + "name": "all-off", + "keys": "Permission,ManageProject::Issue", + "all": "false", }) if err != nil { t.Fatalf("preset failed: %v", err) @@ -205,6 +208,45 @@ func TestMessageSettingsPresetPostsSelectedKeys(t *testing.T) { } } +func TestSelectKeysRequiresSelector(t *testing.T) { + registry := buildRegistry(mustCatalogResponse(t, catalogFixture()), mustUserSettingResponse(t, userSettingFixture())) + if _, _, err := selectKeys(selectorArgs{}, registry); err == nil { + t.Fatal("expected selector validation error") + } +} + +func TestParseGroupFilterRejectsUnknownGroup(t *testing.T) { + registry := buildRegistry(mustCatalogResponse(t, catalogFixture()), nil) + if _, err := parseGroupFilter("UnknownGroup", registry); err == nil { + t.Fatal("expected unknown group error") + } +} + +func TestResolveKeyFilterRejectsAmbiguousShortKey(t *testing.T) { + catalog := &catalogResponse{ + SettingTypes: []catalogGroup{ + { + Type: "TemplateMessageSetting::Normal", + TypeName: "Normal", + Settings: []catalogSettingRow{ + {Name: "Issue", Key: "Issue"}, + }, + }, + { + Type: "TemplateMessageSetting::ManageProject", + TypeName: "Manage", + Settings: []catalogSettingRow{ + {Name: "Issue", Key: "Issue"}, + }, + }, + }, + } + registry := buildRegistry(catalog, nil) + if _, err := resolveKeyFilter("Issue", registry); err == nil { + t.Fatal("expected ambiguous key error") + } +} + func catalogFixture() map[string]interface{} { return map[string]interface{}{ "setting_types": []map[string]interface{}{ @@ -255,14 +297,40 @@ func userSettingFixture() map[string]interface{} { "name": "Alice", }, "notification_body": map[string]interface{}{ - "Normal::Permission": true, - "ManageProject::Issue": true, + "Normal::Permission": true, + "ManageProject::Issue": true, "ManageProject::PullRequest": true, }, "email_body": map[string]interface{}{ - "Normal::Permission": false, - "ManageProject::Issue": false, + "Normal::Permission": false, + "ManageProject::Issue": false, "ManageProject::PullRequest": false, }, } } + +func mustCatalogResponse(t *testing.T, value map[string]interface{}) *catalogResponse { + t.Helper() + raw, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal catalog fixture: %v", err) + } + var result catalogResponse + if err := json.Unmarshal(raw, &result); err != nil { + t.Fatalf("unmarshal catalog fixture: %v", err) + } + return &result +} + +func mustUserSettingResponse(t *testing.T, value map[string]interface{}) *userSettingResponse { + t.Helper() + raw, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal user setting fixture: %v", err) + } + var result userSettingResponse + if err := json.Unmarshal(raw, &result); err != nil { + t.Fatalf("unmarshal user setting fixture: %v", err) + } + return &result +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 17cbb69..86306cb 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -37,55 +37,55 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { tr = translators[0] } groups := map[string][]*common.Shortcut{ - "repo": repo.Shortcuts(tr), - "issue": issue.Shortcuts(tr), - "label": label.Shortcuts(), - "license": license.Shortcuts(), - "member": member.Shortcuts(), - "message-settings": messagesetting.Shortcuts(), - "milestone": milestone.Shortcuts(), - "pipeline": pipeline.Shortcuts(), - "pr": pr.Shortcuts(tr), - "profile": profile.Shortcuts(tr), - "release": release.Shortcuts(tr), - "branch": branch.Shortcuts(tr), - "org": org.Shortcuts(tr), - "user": user.Shortcuts(tr), - "search": search.Shortcuts(tr), - "ci": ci.Shortcuts(tr), - "compare": compare.Shortcuts(), - "dataset": dataset.Shortcuts(tr), - "webhook": webhook.Shortcuts(tr), - "wiki": wiki.Shortcuts(), - "health": health.Shortcuts(tr), - "ignore": ignore.Shortcuts(), - "workflow": workflow.Shortcuts(), + "repo": repo.Shortcuts(tr), + "issue": issue.Shortcuts(tr), + "label": label.Shortcuts(), + "license": license.Shortcuts(), + "member": member.Shortcuts(), + "message-settings": messagesetting.Shortcuts(tr), + "milestone": milestone.Shortcuts(), + "pipeline": pipeline.Shortcuts(), + "pr": pr.Shortcuts(tr), + "profile": profile.Shortcuts(tr), + "release": release.Shortcuts(tr), + "branch": branch.Shortcuts(tr), + "org": org.Shortcuts(tr), + "user": user.Shortcuts(tr), + "search": search.Shortcuts(tr), + "ci": ci.Shortcuts(tr), + "compare": compare.Shortcuts(), + "dataset": dataset.Shortcuts(tr), + "webhook": webhook.Shortcuts(tr), + "wiki": wiki.Shortcuts(), + "health": health.Shortcuts(tr), + "ignore": ignore.Shortcuts(), + "workflow": workflow.Shortcuts(), } descriptions := map[string]string{ - "repo": tr.T("cmd.repo.short"), - "issue": tr.T("cmd.issue.short"), - "label": "Issue label operations", - "license": "License operations", - "member": "Repository member operations", - "message-settings": "Message settings operations", - "milestone": "Milestone operations", - "pipeline": "Pipeline operations", - "pr": tr.T("cmd.pr.short"), - "profile": tr.T("cmd.profile.short"), - "release": tr.T("cmd.release.short"), - "branch": tr.T("cmd.branch.short"), - "org": tr.T("cmd.org.short"), - "user": tr.T("cmd.user.short"), - "search": tr.T("cmd.search.short"), - "ci": tr.T("cmd.ci.short"), - "compare": "Compare branches, tags, or commits", - "dataset": tr.T("cmd.dataset.short"), - "webhook": tr.T("cmd.webhook.short"), - "wiki": "Wiki page management", - "health": "Project health data collection", - "ignore": tr.T("cmd.ignore.short"), - "workflow": "AI agent workflow analysis", + "repo": tr.T("cmd.repo.short"), + "issue": tr.T("cmd.issue.short"), + "label": "Issue label operations", + "license": "License operations", + "member": "Repository member operations", + "message-settings": tr.T("cmd.message_settings.short"), + "milestone": "Milestone operations", + "pipeline": "Pipeline operations", + "pr": tr.T("cmd.pr.short"), + "profile": tr.T("cmd.profile.short"), + "release": tr.T("cmd.release.short"), + "branch": tr.T("cmd.branch.short"), + "org": tr.T("cmd.org.short"), + "user": tr.T("cmd.user.short"), + "search": tr.T("cmd.search.short"), + "ci": tr.T("cmd.ci.short"), + "compare": "Compare branches, tags, or commits", + "dataset": tr.T("cmd.dataset.short"), + "webhook": tr.T("cmd.webhook.short"), + "wiki": "Wiki page management", + "health": "Project health data collection", + "ignore": tr.T("cmd.ignore.short"), + "workflow": "AI agent workflow analysis", } for name, shortcuts := range groups {