gitlink-cli/doc/reading_notes/04_webhook.md

510 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 逐行讲解 shortcuts/webhook/webhook.go面向 Go 小白)
## 文件概述
这个文件实现了 **Webhook 管理**功能,可以对 GitLink 仓库的 Webhook 进行增删改查操作。
---
## 一、包声明和导入
```go
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 事件类型
```go
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`:标签创建/删除事件
---
## 三、事件验证函数
```go
func isEventSupported(event string) bool {
for _, supported := range supportedEvents {
if event == supported {
return true
}
}
return false
}
```
**功能**:检查某个事件类型是否被支持
**工作原理**:遍历 `supportedEvents` 数组,逐一比对,如果找到匹配项就返回 `true`,否则返回 `false`
---
## 四、解析事件字符串
```go
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"`)解析成事件数组
**逐行解读**
1. 如果输入为空,返回默认值 `["push"]`
2. 使用 `strings.Split` 按逗号分割字符串
3. 遍历每个事件,用 `strings.TrimSpace` 去掉前后空格
4.`isEventSupported` 验证有效性,有效才加入结果数组
5. 返回过滤后的有效事件数组
---
## 五、API 路径构建函数
```go
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 主函数
```go
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
// list 命令
// create 命令
// update 命令
// delete 命令
// test 命令
// info 命令
// events 命令
}
}
```
**功能**:返回所有 Webhook 相关的 CLI 命令列表
这个函数是整个文件的核心它定义了7个命令
1. `list` - 列出所有 Webhook
2. `create` - 创建新 Webhook
3. `update` - 更新现有 Webhook
4. `delete` - 删除 Webhook
5. `test` - 测试 Webhook 发送
6. `info` - 查看 Webhook 详情
7. `events` - 列出所有支持的事件类型
---
## 七、命令详解
### 7.1 list 命令
```go
{
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条
**执行流程**
1. 调用 `ctx.ResolveOwnerRepo()` 解析仓库信息
2. 创建 URL 查询参数 `url.Values{}`
3. 设置 `page``limit` 参数
4. 调用 `CallAPIWithQuery` 发送 GET 请求到 `/v1/owner/repo/webhooks`
5. 返回结果给用户
---
### 7.2 create 命令
```go
{
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)
},
}
```
**执行流程**
1. 解析仓库信息
2. 必须获取 `--url` 参数(用 `RequireArg`,如果没提供会报错)
3. 解析事件类型
4. 创建 payload 映射,包含必填字段
5. 添加可选的 secret 和 description
6. 发送 POST 请求创建 Webhook
---
### 7.3 update 命令
```go
{
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 命令
```go
{
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(...))
},
}
```
**亮点**:删除操作有一个**双重验证**机制:
1. 先调用 DELETE 请求
2. 如果 DELETE 返回错误,再调用 GET 请求检查 Webhook 是否还存在
3. 如果 GET 也失败,说明 Webhook 已经被删除了,视为成功
---
### 7.5 test 命令
```go
{
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 命令
```go
{
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 命令
```go
{
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 事件类型及其描述
---
## 八、事件描述函数
```go
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{} 类型
```go
payload := map[string]interface{}{
"url": webhookURL,
"http_method": "POST",
"active": true,
}
```
这是一个**万能类型**,可以存储任意类型的值:
- `"url"` 对应字符串
- `"active"` 对应布尔值
- `"events"` 对应字符串数组
### 2. 字符串拼接
```go
fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
```
类似 Python 的 `f"/v1/{owner}/{repo}"`,用 `%s` 占位符
### 3. 错误包装
```go
return fmt.Errorf("获取 Webhook 列表失败: %w", err)
```
`%w` 是 Go 1.13+ 的错误包装语法,保留原始错误信息
### 4. 函数作为参数
```go
Run: func(ctx *common.RuntimeContext) error {
// 匿名函数
}
```
这是一个**匿名函数**,作为 `Shortcut` 结构体的 `Run` 字段值
### 5. 字符串分割
```go
events := strings.Split(eventsStr, ",")
```
按逗号分割字符串,返回字符串数组