13 KiB
逐行讲解 shortcuts/webhook/webhook.go(面向 Go 小白)
文件概述
这个文件实现了 Webhook 管理功能,可以对 GitLink 仓库的 Webhook 进行增删改查操作。
一、包声明和导入
package webhook
import (
"fmt"
"net/url"
"strings"
clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"
"github.com/gitlink-org/gitlink-cli/internal/output"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
| 导入库 | 作用 |
|---|---|
fmt |
格式化输出,用于拼接字符串和格式化错误信息 |
net/url |
URL 相关操作,用于构建查询参数 |
strings |
字符串处理,用于分割、修剪等操作 |
clierrors |
自定义 CLI 错误类型,用于返回友好的错误提示 |
output |
输出格式化,用于返回统一格式的结果 |
common |
公共工具包,包含 Shortcut、RuntimeContext 等核心类型 |
二、支持的 Webhook 事件类型
var supportedEvents = []string{
"push",
"pull_request",
"issue",
"issue_assign",
"issue_comment",
"pull_request_assign",
"pull_request_comment",
"merge_request",
"repository",
"branch",
"tag",
}
这是一个全局变量,定义了 GitLink 支持的所有 Webhook 事件类型:
push:代码推送事件pull_request:PR 事件issue:Issue 事件issue_assign:Issue 分配事件issue_comment:Issue 评论事件pull_request_assign:PR 分配事件pull_request_comment:PR 评论事件merge_request:合并请求事件repository:仓库事件branch:分支创建/删除事件tag:标签创建/删除事件
三、事件验证函数
func isEventSupported(event string) bool {
for _, supported := range supportedEvents {
if event == supported {
return true
}
}
return false
}
功能:检查某个事件类型是否被支持
工作原理:遍历 supportedEvents 数组,逐一比对,如果找到匹配项就返回 true,否则返回 false
四、解析事件字符串
func parseEvents(eventsStr string) []string {
if eventsStr == "" {
return []string{"push"} // 默认事件
}
events := strings.Split(eventsStr, ",")
var validEvents []string
for _, event := range events {
event = strings.TrimSpace(event)
if isEventSupported(event) {
validEvents = append(validEvents, event)
}
}
return validEvents
}
功能:把用户输入的逗号分隔的事件字符串(如 "push,pull_request")解析成事件数组
逐行解读:
- 如果输入为空,返回默认值
["push"] - 使用
strings.Split按逗号分割字符串 - 遍历每个事件,用
strings.TrimSpace去掉前后空格 - 用
isEventSupported验证有效性,有效才加入结果数组 - 返回过滤后的有效事件数组
五、API 路径构建函数
func webhookRepoPath(ctx *common.RuntimeContext) string {
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
}
功能:构建 Webhook API 的基础路径
参数:ctx 是运行时上下文,包含 Owner(仓库所有者)和 Repo(仓库名)
返回值:类似 /v1/owner/repo 的字符串
注意:注释说明了 BaseURL 已经包含 /api 前缀,所以这里不需要再加
六、Shortcuts 主函数
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
// list 命令
// create 命令
// update 命令
// delete 命令
// test 命令
// info 命令
// events 命令
}
}
功能:返回所有 Webhook 相关的 CLI 命令列表
这个函数是整个文件的核心,它定义了7个命令:
list- 列出所有 Webhookcreate- 创建新 Webhookupdate- 更新现有 Webhookdelete- 删除 Webhooktest- 测试 Webhook 发送info- 查看 Webhook 详情events- 列出所有支持的事件类型
七、命令详解
7.1 list 命令
{
Name: "list",
Description: "List all webhooks for a repository",
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 {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
q.Set("page", ctx.Arg("page"))
q.Set("limit", ctx.Arg("limit"))
env, err := ctx.CallAPIWithQuery("GET", webhookRepoPath(ctx)+"/webhooks", q)
if err != nil {
return fmt.Errorf("获取 Webhook 列表失败: %w", err)
}
return ctx.Output(env)
},
}
Flags 参数说明:
--page/-p:页码,默认第1页--limit/-l:每页条数,默认20条
执行流程:
- 调用
ctx.ResolveOwnerRepo()解析仓库信息 - 创建 URL 查询参数
url.Values{} - 设置
page和limit参数 - 调用
CallAPIWithQuery发送 GET 请求到/v1/owner/repo/webhooks - 返回结果给用户
7.2 create 命令
{
Name: "create",
Description: "Create a new webhook",
Flags: []common.Flag{
{Name: "url", Short: "u", Usage: "Webhook callback URL", Required: true},
{Name: "events", Short: "e", Usage: "Trigger events", Default: "push"},
{Name: "active", Usage: "Webhook active status", Default: "true"},
{Name: "secret", Usage: "Webhook secret for HMAC verification"},
{Name: "description", Short: "d", Usage: "Webhook description"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
webhookURL, err := ctx.RequireArg("url", "--url https://example.com/hook")
if err != nil {
return err
}
events := parseEvents(ctx.Arg("events"))
if len(events) == 0 {
return clierrors.InputError(...)
}
payload := map[string]interface{}{
"url": webhookURL,
"http_method": "POST",
"active": true,
"content_type": "json",
}
// 添加可选参数
if len(events) > 0 {
payload["events"] = events
}
if secret := ctx.Arg("secret"); secret != "" {
payload["secret"] = secret
}
if description := ctx.Arg("description"); description != "" {
payload["description"] = description
}
env, err := ctx.CallAPI("POST", webhookRepoPath(ctx)+"/webhooks", payload)
if err != nil {
return fmt.Errorf("创建 Webhook 失败: %w", err)
}
return ctx.Output(env)
},
}
执行流程:
- 解析仓库信息
- 必须获取
--url参数(用RequireArg,如果没提供会报错) - 解析事件类型
- 创建 payload 映射,包含必填字段
- 添加可选的 secret 和 description
- 发送 POST 请求创建 Webhook
7.3 update 命令
{
Name: "update",
Description: "Update an existing webhook",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
{Name: "url", Short: "u", Usage: "Webhook callback URL"},
{Name: "events", Short: "e", Usage: "Trigger events"},
{Name: "active", Usage: "Webhook active status"},
{Name: "content_type", Usage: "Content type"},
{Name: "secret", Usage: "Webhook secret"},
{Name: "description", Short: "d", Usage: "Webhook description"},
},
Run: func(ctx *common.RuntimeContext) error {
// ... 解析仓库和 ID
webhookURL := ctx.Arg("url")
if webhookURL == "" {
// 如果用户没提供 URL,先获取当前 URL
getEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
// ... 解析响应获取当前 URL
webhookURL = currentURL
}
payload["url"] = webhookURL
// ... 发送 PUT 请求
},
}
亮点:如果用户没有提供新的 URL,会自动调用 GET API 获取当前 URL,这样就不需要用户重复输入
7.4 delete 命令
{
Name: "delete",
Description: "Delete a webhook",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
// ... 解析仓库和 ID
_, delErr := ctx.CallAPI("DELETE", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
if delErr != nil {
// 验证是否真的删除成功
_, viewErr := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
if viewErr != nil {
// GET 也失败,说明 Webhook 确实不存在了,删除成功
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
"message": "Webhook deleted successfully",
}, nil))
}
return fmt.Errorf("删除 Webhook 失败: %w", delErr)
}
return ctx.Output(output.SuccessEnvelope(...))
},
}
亮点:删除操作有一个双重验证机制:
- 先调用 DELETE 请求
- 如果 DELETE 返回错误,再调用 GET 请求检查 Webhook 是否还存在
- 如果 GET 也失败,说明 Webhook 已经被删除了,视为成功
7.5 test 命令
{
Name: "test",
Description: "Test a webhook delivery",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
{Name: "event", Short: "e", Usage: "Event type to test", Default: "push"},
},
Run: func(ctx *common.RuntimeContext) error {
// ... 解析参数
eventType := ctx.Arg("event")
if !isEventSupported(eventType) {
return clierrors.InputError(...)
}
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/webhooks/%s/tests", webhookRepoPath(ctx), webhookID), nil)
// ...
},
}
功能:向指定的 Webhook 发送测试请求,验证 Webhook 是否正常工作
7.6 info 命令
{
Name: "info",
Description: "Show webhook details",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
// ... 调用 GET /v1/owner/repo/webhooks/{id}
},
}
功能:查看单个 Webhook 的详细信息
7.7 events 命令
{
Name: "events",
Description: "List all supported event types for webhooks",
Run: func(ctx *common.RuntimeContext) error {
eventInfo := make([]map[string]interface{}, 0)
for _, event := range supportedEvents {
eventInfo = append(eventInfo, map[string]interface{}{
"event": event,
"supported": true,
"description": getEventDescription(event),
})
}
return ctx.Output(output.SuccessEnvelope(eventInfo, nil))
},
}
功能:列出所有支持的 Webhook 事件类型及其描述
八、事件描述函数
func getEventDescription(event string) string {
descriptions := map[string]string{
"push": "Code push events",
"pull_request": "Pull request events",
"issue": "Issue events",
// ... 其他事件描述
}
if desc, ok := descriptions[event]; ok {
return desc
}
return "Custom event"
}
功能:返回事件类型的英文描述
工作原理:使用 map 查找事件对应的描述,如果找不到就返回 "Custom event"
九、完整调用流程
用户命令 (gitlink webhook list)
↓
解析命令行参数
↓
Shortcuts() 返回命令列表
↓
匹配到 "list" 命令
↓
执行 Run 函数
↓
ctx.ResolveOwnerRepo() → 解析仓库信息
↓
ctx.CallAPIWithQuery() → 调用 HTTP 客户端
↓
内部调用 client.Do() → 发送 GET 请求
↓
解析响应 → ctx.Output() → 格式化输出给用户
十、Go 语言知识点
1. map[string]interface{} 类型
payload := map[string]interface{}{
"url": webhookURL,
"http_method": "POST",
"active": true,
}
这是一个万能类型,可以存储任意类型的值:
"url"对应字符串"active"对应布尔值"events"对应字符串数组
2. 字符串拼接
fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
类似 Python 的 f"/v1/{owner}/{repo}",用 %s 占位符
3. 错误包装
return fmt.Errorf("获取 Webhook 列表失败: %w", err)
%w 是 Go 1.13+ 的错误包装语法,保留原始错误信息
4. 函数作为参数
Run: func(ctx *common.RuntimeContext) error {
// 匿名函数
}
这是一个匿名函数,作为 Shortcut 结构体的 Run 字段值
5. 字符串分割
events := strings.Split(eventsStr, ",")
按逗号分割字符串,返回字符串数组