feat(message-settings): 增强消息通知设置快捷命令交付质量
This commit is contained in:
parent
cf7dab27af
commit
1cb9a6e6ef
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "组织名称",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue